diff --git a/.agents/skills/nemoclaw-contributor-create-pr/SKILL.md b/.agents/skills/nemoclaw-contributor-create-pr/SKILL.md index 356c94a411..06a7e8ef35 100644 --- a/.agents/skills/nemoclaw-contributor-create-pr/SKILL.md +++ b/.agents/skills/nemoclaw-contributor-create-pr/SKILL.md @@ -51,7 +51,7 @@ Run the docs and hook checks instead: ```bash npx prek run --all-files -make docs +npm run docs ``` If a required check fails, fix the issue before creating the PR. @@ -134,7 +134,7 @@ Use the exact template structure below. Fill in each section based on the diff ( - [ ] Tests added or updated for new or changed behavior - [ ] No secrets, API keys, or credentials committed - [ ] Docs updated for user-facing behavior changes -- [ ] `make docs` builds without warnings (doc changes only) +- [ ] `npm run docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) @@ -197,7 +197,7 @@ Created PR [#NNN](https://github.com/NVIDIA/NemoClaw/pull/NNN) - **Do not invent your own PR body format.** Use the template from Step 5 exactly. - **Do not omit sections.** Even if a section is not applicable, keep it with the "Skip if..." comment. -- **Do not check boxes for steps you did not run.** If you did not run `make docs`, leave that box unchecked. +- **Do not check boxes for steps you did not run.** If you did not run `npm run docs`, leave that box unchecked. - **Do not run the full test suite for doc-only changes by default.** Run docs and hook checks instead, and leave `npm test` unchecked unless you actually ran it. - **Do not forget the DCO sign-off.** CI will reject the PR without it. - **Do not forget `--assignee @me`.** Every PR must be assigned to its creator. diff --git a/.agents/skills/nemoclaw-contributor-update-docs/SKILL.md b/.agents/skills/nemoclaw-contributor-update-docs/SKILL.md index acd50e3bb3..566bc41bab 100644 --- a/.agents/skills/nemoclaw-contributor-update-docs/SKILL.md +++ b/.agents/skills/nemoclaw-contributor-update-docs/SKILL.md @@ -50,7 +50,7 @@ git log -50 --oneline --no-merges Filter to commits that are likely to affect docs. Apply every rule below before proceeding. A commit excluded by any rule must not produce doc changes. -1. **Commit type**: `feat`, `fix`, `refactor`, `perf` commits often change behavior. `docs` commits are already doc changes. `chore`, `ci`, `test` commits rarely need doc updates. +1. **Commit type**: `feat`, `fix`, `refactor`, `perf` commits often change behavior. `docs` commits are already doc changes, but still need a review pass when they fall in the scanned range. 2. **Files changed**: Changes to `nemoclaw/src/`, `nemoclaw-blueprint/`, `bin/`, `scripts/`, or policy-related code are high-signal. 3. **Ignore**: Changes limited to `test/`, `.github/`, or internal-only modules. 4. **Skip list**: Exclude any commit whose short hash appears in `skip-commits`, or whose commit message or changed file paths contain a `skip-features` substring. Report skipped commits in the final summary under a "Skipped (docs-skip)" heading. @@ -79,6 +79,8 @@ For each relevant commit, determine which doc page(s) it affects. Use this mappi | Inference-related changes | `docs/inference/inference-options.mdx` | If a commit does not map to any existing page but introduces a user-visible concept, flag it as needing a new page. +If a commit already changes files under `docs/`, include those pages in the target page list and run a docs review or edit pass against them using the style guidance in Step 5. +Do not assume an existing doc change is complete, correctly placed, or style-compliant just because it landed with the source commit. ## Step 3: Read the Commit Details @@ -98,6 +100,7 @@ Extract: ## Step 4: Read the Current Doc Page Before editing, read the full target doc page to understand its current content and structure. +For target pages that were already changed by a scanned commit, compare the committed doc diff against the source behavior and the style guidance before deciding whether to edit further. Identify where the new content should go. Follow the page's existing structure. @@ -161,9 +164,8 @@ Skip this step when the user only asked for ordinary doc catch-up and no release If the user invoked this skill for release prep, finish the release-specific doc work before verification: -1. Make any requested doc version bumps in `versions1.json` and `project.json` in the `docs/` directory. -2. Determine the release label from the release version. Release labels use `vX.Y.Z` format. For example, if `docs/project.json` has `"version": "0.0.37"`, the release label is `v0.0.37`. Use the version requested by the user if one was provided; otherwise use the version in `docs/project.json` after the bump. -3. Refresh the NemoClaw user skills: +1. Determine the release label from the release version requested by the user. Release labels use `vX.Y.Z` format. For example, release `0.0.37` uses label `v0.0.37`. If the user did not provide a release version, ask for it before opening the release-prep PR. +2. Refresh the NemoClaw user skills: ```bash python3 scripts/docs-to-skills.py docs/ .agents/skills/ --prefix nemoclaw-user --doc-platform fern-mdx @@ -174,7 +176,7 @@ If the user invoked this skill for release prep, finish the release-specific doc After making changes, build the docs locally: ```bash -make docs +npm run docs ``` Check for: @@ -213,10 +215,10 @@ User says: "Catch up the docs for everything merged since v0.1.0." 3. Map each to a doc page. 4. Read the commit diffs and current doc pages. 5. Draft doc updates reflecting the source code changes in the commits following the style guide. -6. **Release prep only:** Apply release-prep version bumps if the user requested release prep. +6. **Release prep only:** Determine the release label from the user-requested release version. 7. **Release prep only:** Run `python3 scripts/docs-to-skills.py docs/ .agents/skills/ --prefix nemoclaw-user --doc-platform fern-mdx`. 8. Present the summary. -9. Build with `make docs` to verify. +9. Build with `npm run docs` to verify. 10. **Release prep only:** Commit changes and open a pull request with the `documentation` label and the corresponding `vX.Y.Z` release label. Include a concise summary of the doc updates and a source summary that links each identified merged PR to its matching doc page. Include the PR number, affected doc page, links, and description of the doc change in this shape: ```markdown diff --git a/.agents/skills/nemoclaw-user-configure-inference/SKILL.md b/.agents/skills/nemoclaw-user-configure-inference/SKILL.md index 7a67d233df..f43d2631eb 100644 --- a/.agents/skills/nemoclaw-user-configure-inference/SKILL.md +++ b/.agents/skills/nemoclaw-user-configure-inference/SKILL.md @@ -31,10 +31,36 @@ The onboard wizard detects Ollama automatically when it is installed or running If Ollama is installed but not running, NemoClaw starts it for you. On macOS and Linux, the wizard can also offer to install Ollama when it is not present. +When the host Ollama is below the minimum version NemoClaw expects for its starter models (currently `0.7.0`), the wizard surfaces an explicit **Upgrade Ollama** entry in the provider menu instead of silently reusing the older daemon, and the express setup path resolves to that entry. +The wizard inspects both the CLI binary (`ollama --version`) and the locally running daemon (`/api/version` on `:11434`) so the upgrade entry still appears when only one side is stale, for example a fresh user-local binary paired with the original system daemon. +The gate skips Windows-host Ollama reached from WSL via `host.docker.internal`; the separate **Use / Start / Install Ollama on Windows host** entries handle that case and run their own actions on the Windows side. +On macOS, the wizard runs the platform install or upgrade path with `brew upgrade ollama`. +On Linux, the wizard runs the official `https://ollama.com/install.sh` path. +Upgrades on Linux always take the sudo-driven system path because the sudo-free user-local fallback would leave the existing system daemon on `:11434` serving the stale binary. +If sudo is not available in a non-interactive run, NemoClaw refuses to silently downgrade the path and asks you to rerun interactively or upgrade Ollama manually. +After an upgrade finishes, NemoClaw re-probes the running daemon's `/api/version` and fails the run if the daemon still reports below the minimum. +Fresh installs skip this re-probe because the bundled installers ship a daemon at or above the minimum. On WSL, the wizard can use, start, restart, or install Ollama on the Windows host through PowerShell interop. -On Debian and Ubuntu, the native Linux install path checks for `zstd` before it runs the Ollama installer. -If `zstd` is missing, NemoClaw installs it with `apt-get` and explains the sudo prompt before continuing. -On non-apt Linux distributions, install `zstd` first, then rerun onboarding. + +### Linux Install Modes + +On native Linux, the install path picks between a system install (under `/usr/local`, via the official `https://ollama.com/install.sh`) and a sudo-free user-local install (under `${HOME}/.local`). +NemoClaw selects the mode automatically: + +- Running as root or with passwordless sudo (`sudo -n true` returns 0) selects the system install. +- A non-interactive run (`NEMOCLAW_NON_INTERACTIVE=1` or no TTY on stdin) without passwordless sudo selects the user-local install. + This is the path that lets headless hosts complete onboarding without prompting for a sudo password. +- An interactive shell without passwordless sudo selects the system install and lets the official installer prompt for the password as usual. + +Override the detection with `NEMOCLAW_OLLAMA_INSTALL_MODE=system` or `NEMOCLAW_OLLAMA_INSTALL_MODE=user`. + +The user-local install replicates only the binary extraction step of the official installer. +It downloads the release tarball, extracts it to `${HOME}/.local`, and launches `${HOME}/.local/bin/ollama serve` once. +It does not configure a systemd service, does not create the `ollama` system user, and does not install CUDA drivers, so the daemon must be relaunched manually after a reboot. +NemoClaw also prints a one-line `PATH` hint if `${HOME}/.local/bin` is not already on your `PATH`; you can add `export PATH="${HOME}/.local/bin:$PATH"` to your shell profile to invoke `ollama` directly. + +Both modes rely on `zstd` for archive extraction. On Debian and Ubuntu, the system path uses `sudo apt-get` to install `zstd` automatically and explains the prompt before continuing. +The user-local path cannot bootstrap system packages without elevation, so if `zstd` is missing it prints per-distro install hints and exits — install `zstd` manually, then rerun onboarding. Run the onboard wizard. @@ -44,7 +70,8 @@ $ nemoclaw onboard Select **Local Ollama** from the provider list. NemoClaw lists installed models or offers starter models if none are installed. -On hosts with at least 32 GiB of detected GPU memory, the starter list includes `qwen3.6:35b` and selects it by default. +On hosts where the larger starter models fit the currently available GPU memory, the starter list includes `qwen3.6:35b` and selects it by default. +When another GPU workload is using most of the memory at onboard time, NemoClaw downgrades the menu to the largest model that still fits. It pulls the selected model, loads it into memory, and validates it before continuing. If the selected model declares that it does not support tool calling, onboarding stops with guidance to choose a model whose `ollama show ` capabilities include `tools`. The validation also requires structured chat-completions tool calls. @@ -136,6 +163,8 @@ $ NEMOCLAW_PROVIDER=ollama \ ``` If `NEMOCLAW_MODEL` is not set, NemoClaw selects a default model based on available memory. +If `NEMOCLAW_MODEL` names a known bootstrap model (for example `qwen3.6:35b`) that does not fit the host's currently available GPU memory, NemoClaw warns and falls back to the largest known model that does fit. +Unknown or custom tags (any value the bootstrap registry has not seen) are still passed through; the Ollama runner validates the choice itself. `--yes` (or `NEMOCLAW_YES=1`) authorises the Ollama model download without an interactive confirmation prompt. Under `--non-interactive`, `--yes` (or `NEMOCLAW_YES=1`) is required to authorise the download — onboard exits otherwise, since it cannot prompt. diff --git a/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md b/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md index b4f3960181..b0f3f0fb32 100644 --- a/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md +++ b/.agents/skills/nemoclaw-user-configure-security/references/best-practices.md @@ -174,7 +174,7 @@ The container mounts system directories read-only to prevent the agent from modi ### Agent Config Directory The `/sandbox/.openclaw` directory contains the OpenClaw gateway configuration (model routing, CORS settings, channel config). -The current entrypoint reads the gateway auth token from OpenClaw config when present, exports it as `OPENCLAW_GATEWAY_TOKEN`, and writes it to `/tmp/nemoclaw-proxy-env.sh` so interactive sandbox sessions can reach the gateway through the static `/sandbox/.bashrc` and `/sandbox/.profile` source shims. +The current entrypoint reads the gateway auth token from OpenClaw config when present, exports it as `OPENCLAW_GATEWAY_TOKEN`, and writes it to `/tmp/nemoclaw-proxy-env.sh` so interactive sandbox sessions can reach the gateway through system-wide shell hooks. In root mode, the gateway process still runs as the separate `gateway` user, but the token is intentionally available to sandbox shells for local gateway access. Writable agent state such as plugins, skills, hooks, and workspace metadata lives directly under `/sandbox/.openclaw`. diff --git a/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md b/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md index 2e491e0181..669096f180 100644 --- a/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md +++ b/.agents/skills/nemoclaw-user-deploy-remote/references/sandbox-hardening.md @@ -98,8 +98,8 @@ System paths remain read-only to prevent agents from: - Modifying DNS resolution or TLS trust stores - Tampering with libraries or shell configuration outside `/sandbox` -The image build pre-creates shell init files `.bashrc` and `.profile`. -These files source runtime proxy configuration from `/tmp/nemoclaw-proxy-env.sh`. +The image build pre-creates locked shell init files `.bashrc` and `.profile` without proxy entries. +Runtime proxy configuration is sourced from system-wide shell hooks that read `/tmp/nemoclaw-proxy-env.sh`. ### Landlock Kernel Requirements diff --git a/.agents/skills/nemoclaw-user-manage-policy/references/integration-policy-examples.md b/.agents/skills/nemoclaw-user-manage-policy/references/integration-policy-examples.md index dc1bd1e68e..0403b3e5b6 100644 --- a/.agents/skills/nemoclaw-user-manage-policy/references/integration-policy-examples.md +++ b/.agents/skills/nemoclaw-user-manage-policy/references/integration-policy-examples.md @@ -163,6 +163,20 @@ $ nemoclaw my-assistant policy-add github --yes $ nemoclaw my-assistant policy-add jira --yes ``` +The `jira` preset intentionally allows Node.js access to Atlassian Cloud and does not allow `curl`. +When validating it manually, avoid plain `curl -s` against `auth.atlassian.com`. +Atlassian can return an empty redirect body even when the request succeeds. +Use an explicit status probe instead: + +```console +$ node -e "require('https').get('https://api.atlassian.com', r => console.log(r.statusCode))" +$ curl -sS -o /dev/null -w '%{http_code}' --max-time 10 https://auth.atlassian.com +``` + +Before approval, the curl probe should report `000` or a local policy denial. +After approving the blocked request in OpenShell, it should report an HTTP +status such as `301` or `200`. + Remove access when the task is done: ```console diff --git a/.agents/skills/nemoclaw-user-manage-sandboxes/references/messaging-channels.md b/.agents/skills/nemoclaw-user-manage-sandboxes/references/messaging-channels.md index a675691645..31c17763fb 100644 --- a/.agents/skills/nemoclaw-user-manage-sandboxes/references/messaging-channels.md +++ b/.agents/skills/nemoclaw-user-manage-sandboxes/references/messaging-channels.md @@ -30,6 +30,8 @@ For details, refer to Commands (use the `nemoclaw-user-reference` skill). Telegram uses a bot token from [BotFather](https://t.me/BotFather). Open Telegram, send `/newbot` to [@BotFather](https://t.me/BotFather), follow the prompts, and copy the token. +For Telegram group chats, disable privacy mode before testing group replies: in @BotFather, run `/setprivacy`, choose the bot, then choose **Disable**. +After changing privacy mode, remove the bot from each Telegram group and add it back so Telegram applies the new delivery setting to that group. `TELEGRAM_ALLOWED_IDS` is a comma-separated list of Telegram user IDs for DM access. Group chats stay open by default so rebuilt sandboxes do not silently drop Telegram group messages because of an empty group allowlist. Set `TELEGRAM_REQUIRE_MENTION=1` to make the bot reply in Telegram groups only when users mention it. @@ -190,6 +192,7 @@ Use the matching policy preset (`telegram`, `discord`, `slack`, or `whatsapp`) o ## Tunnel Command When the host has `cloudflared`, `nemoclaw tunnel start` starts a cloudflared tunnel that can expose the dashboard with a public URL. +Set `CLOUDFLARE_TUNNEL_TOKEN` before running the command when you want to use a Cloudflare named tunnel instead of a generated quick-tunnel URL. `nemoclaw tunnel stop` stops the tunnel and asks NemoClaw to stop the in-sandbox gateway for the selected or default sandbox. The older `nemoclaw start` still works as a deprecated alias. diff --git a/.agents/skills/nemoclaw-user-overview/references/how-it-works.md b/.agents/skills/nemoclaw-user-overview/references/how-it-works.md index 71172da7a0..8305f2c2aa 100644 --- a/.agents/skills/nemoclaw-user-overview/references/how-it-works.md +++ b/.agents/skills/nemoclaw-user-overview/references/how-it-works.md @@ -14,7 +14,7 @@ NemoClaw keeps the user workflow on the host while OpenShell enforces the sandbo The gateway sits between NemoClaw control, the sandbox, inference providers, and external integrations. That placement lets NemoClaw configure the environment without giving the agent direct access to host credentials or uncontrolled network egress. -![NemoClaw High-Level Component Diagram](https://docs.nvidia.com/nemoclaw/latest/about/images/nemoclaw-highlevel-component-diagram.html) +![NemoClaw High-Level Component Diagram](images/nemoclaw-highlevel-component-diagram.png) The diagram has the following components: diff --git a/.agents/skills/nemoclaw-user-overview/references/images/nemoclaw-highlevel-component-diagram.png b/.agents/skills/nemoclaw-user-overview/references/images/nemoclaw-highlevel-component-diagram.png new file mode 100644 index 0000000000..e1d4a43fc1 Binary files /dev/null and b/.agents/skills/nemoclaw-user-overview/references/images/nemoclaw-highlevel-component-diagram.png differ diff --git a/.agents/skills/nemoclaw-user-overview/references/release-notes.md b/.agents/skills/nemoclaw-user-overview/references/release-notes.md index 55f8b3e513..a672daf780 100644 --- a/.agents/skills/nemoclaw-user-overview/references/release-notes.md +++ b/.agents/skills/nemoclaw-user-overview/references/release-notes.md @@ -4,13 +4,38 @@ NVIDIA NemoClaw is available in early preview starting March 16, 2026. Use this page to track changes. +## v0.0.51 + +NemoClaw v0.0.51 improves messaging controls, local inference setup, policy validation, and onboarding recovery: + +- Slack setup now supports channel allowlisting. During onboarding, `channels add slack`, and non-interactive rebuilds, set `SLACK_ALLOWED_CHANNELS` to restrict channel `@mention` handling to selected Slack channel IDs. Combine it with `SLACK_ALLOWED_USERS` when you want both channel and member checks. +- Local Ollama setup now detects host installations that are below the minimum supported version and offers an explicit upgrade path. On macOS, NemoClaw uses Homebrew. On Linux, NemoClaw uses the system installer for upgrades and refuses non-interactive upgrade paths that would require a hidden sudo prompt. +- Managed Ollama model selection now uses a memory-aware registry for starter models. If a known bootstrap model does not fit currently available GPU memory, NemoClaw warns and falls back to the largest known model that does fit instead of starting a model that is likely to fail. +- `nemoclaw resources` and `NEMOCLAW_RESOURCE_PROFILE` expose sandbox CPU and memory profiles. Profiles can be selected during onboarding, and `NEMOCLAW_CPU` or `NEMOCLAW_RAM` can override the selected profile for scripted runs. +- Cloudflare named tunnels are supported through `CLOUDFLARE_TUNNEL_TOKEN`. `nemoclaw tunnel start` passes the token through the environment and expects the named tunnel route to already point at the dashboard port. +- Jira policy validation guidance now matches the maintained preset. Use a Node HTTPS status probe for Atlassian API access and an explicit status-only curl probe for `auth.atlassian.com` when validating approved requests manually. +- Onboarding recovers more cleanly across host and runtime edge cases, including root-owned config sync directories, stale dashboard port allocation, unreachable Docker daemons, stale dashboard forwards, default NVIDIA CDI spec directories, and Linux Docker-driver health checks. + +## v0.0.50 + +NemoClaw v0.0.50 focused on onboarding reliability, local inference hardening, messaging diagnostics, and sandbox lifecycle cleanup: + +- `nemoclaw onboard` handles DGX Spark and DGX Station managed vLLM setup more consistently, including restored vLLM menu entries and CPU-fallback detection on Spark hosts. +- Non-interactive Linux Ollama setup can use a sudo-free user-local install path when passwordless sudo is unavailable. The docs now describe `NEMOCLAW_OLLAMA_INSTALL_MODE`, the user-local install trade-offs, and the manual `zstd` requirement. +- Compatible endpoint setup rejects `host.docker.internal` inference URLs because OpenShell sandboxes do not have a portable host-service route through that name. Use Local Ollama's authenticated proxy path or a policy-managed host service instead. +- Telegram setup now surfaces BotFather group privacy guidance. Disable privacy mode, then remove and re-add the bot to each group before testing group delivery. +- Sandbox logs merge OpenClaw gateway output and OpenShell audit events into one stream, and `--tail` applies once to the merged result so policy denials appear beside gateway logs. +- Maintenance commands recover the OpenShell gateway before retrying sandbox-list operations, which makes rebuild, recover, upgrade, and backup flows more resilient after gateway drift. +- NemoClaw no longer writes proxy hooks into sandbox shell startup files. Local proxy configuration stays on supported OpenShell and NemoClaw paths rather than mutating user shell rc files. +- Windows bootstrap installs Ubuntu 24.04 when WSL is present but no Ubuntu distribution is registered. + ## v0.0.49 NemoClaw v0.0.49 is a hardening release focused on reliability, clearer diagnostics, OpenClaw compatibility, and stronger validation coverage: - Gateway failures now fail faster and explain more. `nemoclaw status` classifies gateway probe failures by layer, distinguishing a named gateway port that is not accepting connections, a named gateway that is present but not Connected, the active OpenShell gateway pointing at a different name, and a named gateway that is not configured at all. `nemoclaw connect` exits early with recovery guidance when the OpenShell gateway is down. - Gateway upgrade and fallback paths are more stable. The release hardens older gateway fallback coverage, OpenShell gateway upgrade checks, crash-loop detection tests, and Brev GPU bridge gateway traffic coverage. -- Status, doctor, and `shields status` now report a fresh mutable sandbox as not configured instead of `down`, and `nemoclaw logs --tail ` is locked in as a NemoClaw line count rather than OpenShell's follow-flag pun. `nemoclaw debug --quick` reports restricted kernel-log access as a skipped section instead of surfacing raw `dmesg` permission errors. +- Status and doctor now report a fresh mutable sandbox as not configured instead of `down`, and `nemoclaw logs --tail ` is locked in as a NemoClaw line count rather than OpenShell's follow-flag pun. `nemoclaw debug --quick` reports restricted kernel-log access as a skipped section instead of surfacing raw `dmesg` permission errors. - OpenClaw compatibility is more resilient across runtime changes. Kimi mixed tool calls are normalized more consistently, compatible OpenClaw JSON envelope changes are tolerated in tests, and OpenClaw patch drift is easier to classify during image builds. - Messaging channel removal is now a clean teardown. The sandbox registry and onboard session policy preset state stay in sync so removed presets do not return during later `onboard --resume` or rebuild flows; QR-paired channels also have their durable in-sandbox session directory wiped before the rebuild and removal aborts cleanly when that wipe cannot be confirmed; and `~/.nemoclaw/config.json` is re-synced from the host across every rebuild resume path so the OpenClaw plugin no longer crashes on the Dockerfile placeholder. - Hermes sandboxes apply only the messaging channel policies the operator selects instead of pre-enabling every Hermes messaging provider, and dynamic preset application resolves Hermes-specific policy content so Discord on Hermes no longer falls back to generic Node allowlists. diff --git a/.agents/skills/nemoclaw-user-reference/references/cli-selection-guide.md b/.agents/skills/nemoclaw-user-reference/references/cli-selection-guide.md index 7e6faf4b2c..7b96be6d39 100644 --- a/.agents/skills/nemoclaw-user-reference/references/cli-selection-guide.md +++ b/.agents/skills/nemoclaw-user-reference/references/cli-selection-guide.md @@ -91,7 +91,8 @@ Use `openshell` when the docs explicitly call for a live OpenShell gateway opera ```console $ openshell sandbox list $ openshell sandbox get - $ openshell logs -n 200 --tail + $ openshell logs -n 20 + $ openshell doctor check ``` - Run one-off commands or move files without starting a NemoClaw chat session: @@ -144,8 +145,8 @@ $ openshell sandbox exec -n my-assistant -- cat /tmp/gateway.log Use `nemoclaw status` and `nemoclaw logs` first. They combine NemoClaw registry data, OpenShell state, OpenClaw process health, inference health, policy details, and messaging-channel warnings. -Use `openshell sandbox list`, `openshell sandbox get`, or `openshell logs` when debugging lower-level OpenShell behavior. -When using `openshell logs` directly, `--tail` follows live output and `-n ` controls the line count; NemoClaw's `logs --tail ` is the line-count form, and `logs --follow` opts into streaming. +Use `openshell sandbox list`, `openshell sandbox get`, `openshell logs -n 20`, or `openshell doctor check` when debugging lower-level OpenShell behavior. +When using `openshell logs` directly, `-n ` controls the line count; use `--tail` only when you want live OpenShell log streaming. ### Approve Blocked Network Requests diff --git a/.agents/skills/nemoclaw-user-reference/references/commands.md b/.agents/skills/nemoclaw-user-reference/references/commands.md index 0fbe480eda..b6f96b32ae 100644 --- a/.agents/skills/nemoclaw-user-reference/references/commands.md +++ b/.agents/skills/nemoclaw-user-reference/references/commands.md @@ -38,6 +38,17 @@ Print the installed NemoClaw CLI version. $ nemoclaw --version ``` +### `nemoclaw resources` + +Display host hardware inventory and configured sandbox resource profiles. +Use `--json` for machine-readable CPU, memory, GPU, Kubernetes allocatable-capacity, and profile data. + +```console +$ nemoclaw resources [--json] +``` + +If the gateway is not running, Kubernetes allocatable fields are omitted and host CPU/RAM totals are still shown. + ### `nemoclaw onboard` Run the interactive setup wizard (recommended for new installs). @@ -962,12 +973,24 @@ For a remote Brev instance, SSH to the instance and run `openshell term` there, ### `nemoclaw tunnel start` -Start optional host auxiliary services. This is the cloudflared tunnel when `cloudflared` is installed (for a public URL to the dashboard). Channel messaging (Telegram, Discord, Slack) is not started here; it is configured during `nemoclaw onboard` and runs through OpenShell-managed constructs. +Start optional host auxiliary services. +This is the cloudflared tunnel when `cloudflared` is installed, which exposes the dashboard with a public URL. +Channel messaging (Telegram, Discord, Slack) is not started here; it is configured during `nemoclaw onboard` and runs through OpenShell-managed constructs. ```console $ nemoclaw tunnel start ``` +By default, NemoClaw starts a Cloudflare quick tunnel and prints the generated `*.trycloudflare.com` URL when `cloudflared` reports it. +Set `CLOUDFLARE_TUNNEL_TOKEN` to start a Cloudflare named tunnel instead. +The named tunnel hostname and `localhost:` route must already be configured in the Cloudflare dashboard. +NemoClaw passes the token to `cloudflared` through the `TUNNEL_TOKEN` environment variable, so the token does not appear in the `cloudflared` command-line arguments. + +```console +$ export CLOUDFLARE_TUNNEL_TOKEN= +$ nemoclaw tunnel start +``` + `nemoclaw start` remains as a deprecated alias that prints a warning and delegates to `tunnel start`. ### `nemoclaw tunnel stop` @@ -1215,7 +1238,7 @@ Set them before running `nemoclaw onboard`. | Variable | Format | Effect | |----------|--------|--------| -| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermesProvider`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. Aliases: `cloud` → `build`, `nim` → `nim-local`, `hermes` / `hermes-provider` / `nous` / `nous-portal` → `hermesProvider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | +| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. Aliases: `cloud` → `build`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | | `NEMOCLAW_HERMES_AUTH_METHOD` | `oauth` | Selects Hermes Provider authentication in non-interactive onboarding. Valid values: `oauth`, `nous-portal-oauth`, `api-key`, `nous-api-key`. | | `NEMOCLAW_HERMES_AUTH` | same as `NEMOCLAW_HERMES_AUTH_METHOD` | Back-compatible alias for Hermes Provider authentication selection. | | `NEMOCLAW_NOUS_AUTH_METHOD` | same as `NEMOCLAW_HERMES_AUTH_METHOD` | Nous-specific alias for Hermes Provider authentication selection. | @@ -1228,6 +1251,7 @@ Set them before running `nemoclaw onboard`. | `NEMOCLAW_REASONING` | `true` or `false` | Overrides the model's reasoning-mode flag in the built OpenClaw config. | | `NEMOCLAW_AGENT_HEARTBEAT_EVERY` | duration with `s`, `m`, or `h` suffix (for example `30m`, `1h`, or `0m`) | Overrides `agents.defaults.heartbeat.every` in the built OpenClaw config. Set `0m` to disable periodic agent turns. | | `NEMOCLAW_OLLAMA_REQUIRE_TOOLS` | `0` to disable, anything else to keep the default | When set to `0`, skips the Ollama tool-calling capability check during local-inference onboarding. | +| `NEMOCLAW_OLLAMA_INSTALL_MODE` | `system`, `user`, or empty/unset | Pins the Linux Ollama install location; see the Linux Ollama install mode details below. | | `NEMOCLAW_PROXY_HOST` | hostname or IP | Overrides the sandbox-side outbound HTTP proxy host. Defaults to `10.200.0.1`. | | `NEMOCLAW_PROXY_PORT` | integer port | Overrides the sandbox-side outbound HTTP proxy port. Defaults to `3128`. | | `NEMOCLAW_OPENSHELL_BIN` | path | Overrides the `openshell` binary the CLI invokes. Defaults to `openshell` (resolved via `PATH`). | @@ -1237,6 +1261,19 @@ Set them before running `nemoclaw onboard`. | `NEMOCLAW_VLLM_MODEL` | registry slug or Hugging Face model id | Selects the model the managed-vLLM install path serves. Recognised slugs: `qwen3.6-27b`, `nemotron-3-nano-4b`, `deepseek-r1-distill-70b`. Unset uses the per-platform profile default. Gated models (e.g. `deepseek-r1-distill-70b`) require `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN`. | | `NEMOCLAW_MODEL_ROUTER_PYTHON` | absolute path | Pins the host Python interpreter used to create the Model Router virtual environment. Strict. NemoClaw probes only that interpreter and aborts with the failure reason if it does not qualify, rather than silently falling back to another python. Relative command names such as `python3.12` are rejected. When unset, NemoClaw probes `python3.13`, `python3.12`, `python3.11`, `python3.10`, and bare `python3`, retains every interpreter whose version is in `[3.10, 3.14)` and whose `ensurepip`, `pyexpat`, `ssl`, and `venv` stdlib modules import cleanly, and tries `python -m venv` on each in priority order until one succeeds. Set the pin when the auto-discovered interpreter is broken (for example, Homebrew `python@3.14` with a `pyexpat` dlopen mismatch on macOS). | +#### Linux Ollama install mode details + +Set `NEMOCLAW_OLLAMA_INSTALL_MODE=system` to run the official `https://ollama.com/install.sh` installer, which uses sudo, writes to `/usr/local`, and configures systemd. +Set `NEMOCLAW_OLLAMA_INSTALL_MODE=user` to extract the release tarball to `${HOME}/.local` without sudo and launch the daemon manually without systemd persistence. +Leave `NEMOCLAW_OLLAMA_INSTALL_MODE` empty or unset to let NemoClaw auto-detect the mode. +Auto-detection selects `system` when the current user is root or passwordless `sudo` works. +Auto-detection selects `user` in non-interactive runs without passwordless `sudo`. +An interactive shell falls back to `system` so the official installer can prompt for the password. +NemoClaw rejects any other value. +On upgrades, NemoClaw rejects `user` because a user-local install cannot replace the system daemon on `:11434`. +On upgrades, NemoClaw also rejects `system` under `NEMOCLAW_NON_INTERACTIVE=1` when passwordless `sudo` is unavailable because the installer would hang on a hidden sudo prompt. +The run exits with an actionable diagnostic instead. + ### Onboarding Behavior Flags These flags toggle optional behaviors during onboarding; set them before running `nemoclaw onboard`. @@ -1252,6 +1289,9 @@ These flags toggle optional behaviors during onboarding; set them before running | `NEMOCLAW_OVERLAY_SNAPSHOTTER` | snapshotter name | Selects the containerd overlay snapshotter for sandbox builds. Empty (default) preserves containerd's choice. | | `NEMOCLAW_SKIP_TELEGRAM_REACHABILITY` | `1` to enable | Skips the Telegram bot reachability probe during onboard (useful in restricted networks). | | `NEMOCLAW_CONFIG_ACCEPT_NEW_PATH` | `1` to enable | Accepts a new sandbox config path without an interactive prompt when the stored path differs from the discovered one. | +| `NEMOCLAW_RESOURCE_PROFILE` | profile name or `default` | Selects a sandbox CPU/RAM resource profile from the blueprint during onboarding. `default` means no resource preference, so NemoClaw passes no OpenShell CPU or memory flags. Unknown names fail fast. | +| `NEMOCLAW_CPU` | percentage or Kubernetes CPU quantity | Overrides the selected profile's CPU size passed to OpenShell `--cpu`. Percentages resolve against detected capacity. | +| `NEMOCLAW_RAM` | percentage or Kubernetes memory quantity | Overrides the selected profile's memory size passed to OpenShell `--memory`. Percentages resolve against detected capacity. | | `NEMOCLAW_SANDBOX_GPU` | `auto`, `1`, or `0` | Controls sandbox GPU passthrough during onboarding. `auto` enables GPU passthrough when an NVIDIA GPU is detected, `1` requires GPU passthrough, and `0` forces CPU-only sandbox creation. | | `NEMOCLAW_SANDBOX_GPU_DEVICE` | OpenShell GPU device selector | Selects the GPU device passed with `openshell sandbox create --gpu-device`. Requires explicit sandbox GPU enablement with `NEMOCLAW_SANDBOX_GPU=1` (or `--sandbox-gpu` for CLI-driven onboarding); otherwise onboarding rejects the selector instead of treating it as an implicit opt-in. | | `NEMOCLAW_DOCKER_GPU_PATCH` | `0` to disable, anything else to keep the default | Controls the Linux Docker-driver GPU sandbox compatibility patch. Set to `0` only as an escape hatch when the patch fails and you need onboarding to continue without patching the GPU sandbox container. | diff --git a/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md b/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md index 15a349d2b1..3f586df360 100644 --- a/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md +++ b/.agents/skills/nemoclaw-user-reference/references/troubleshooting.md @@ -885,6 +885,11 @@ Check the gateway logs and blocked-request output with `openshell term`, and loo Bot tokens for Telegram (`getUpdates`), Discord (gateway), and Slack (Socket Mode) only allow one active consumer per token. If two NemoClaw sandboxes are configured with the same bot token, each one kicks the other off its polling connection and neither delivers messages. `nemoclaw status` still reports the bridge as running because the gateway process itself is alive. +For Telegram group chats, first check BotFather privacy mode. +New Telegram bots default to privacy mode enabled, which prevents group messages from reaching `getUpdates` even when the user mentions the bot. +In @BotFather, run `/setprivacy`, choose the bot, and choose **Disable**. +Then remove the bot from the affected group and add it back; Telegram applies the privacy-mode change to group delivery only after the bot rejoins. + To diagnose, open a shell in the sandbox and inspect the gateway log: ```console @@ -1068,6 +1073,38 @@ $ nemoclaw onboard Docker Desktop, WSL, and hosts without the OpenShell Docker network use different routing models. In those cases NemoClaw treats an unavailable sandbox-side probe as non-blocking and relies on the regular proxy health check. +### `host.docker.internal` does not reliably reach the host from the sandbox + +Configuring an inference provider with a base URL like +`http://host.docker.internal:11434/v1` does not reliably reach a host Ollama +service from inside the OpenShell sandbox. +OpenShell runs sandboxes inside a k3s network, where `host.docker.internal` is +not a portable host-service route. Depending on the platform, it may fail DNS +resolution or resolve to an internal gateway/bridge address where the host's +port `11434` is not forwarded. The sandbox then sees a DNS failure or +`connection refused`: + +```console +$ getent hosts host.docker.internal +172.17.0.1 host.docker.internal host.openshell.internal +$ no_proxy=host.docker.internal curl -v http://host.docker.internal:11434/api/tags +* connect to 172.17.0.1 port 11434 failed: Connection refused +``` + +For local Ollama, use the auth-proxy URL that NemoClaw's "Local Ollama" onboard +option configures automatically: + +```text +http://host.openshell.internal:11435/v1 +``` + +`host.openshell.internal` resolves to the same gateway IP, and the +[token-gated Ollama auth proxy](#ollama-auth-proxy-did-not-start) binds port +`11435` there and forwards requests to `127.0.0.1:11434` on the host. +If you need a different host service exposed to the sandbox, route it through +the OpenShell gateway rather than relying on `host.docker.internal`. +See issue [#3136](https://github.com/NVIDIA/NemoClaw/issues/3136). + ### Local inference health check resolves to IPv6 Local inference health checks now use `127.0.0.1` instead of `localhost`. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c43bc27011..7a754447dd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,7 +22,7 @@ - [ ] Tests added or updated for new or changed behavior - [ ] No secrets, API keys, or credentials committed - [ ] Docs updated for user-facing behavior changes -- [ ] `make docs` builds without warnings (doc changes only) +- [ ] `npm run docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) diff --git a/AGENTS.md b/AGENTS.md index ea315b8773..2b5480f862 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,8 +44,8 @@ This repo ships agent skills under `.agents/skills/`, organized into three audie | Run all hooks manually | `npx prek run --all-files` | | Type-check CLI | `npm run typecheck:cli` | | Auto-format | `make format` | -| Build docs | `make docs` | -| Serve docs locally | `make docs-live` | +| Build docs | `npm run docs` | +| Serve docs locally | `npm run docs:live` | ## Key Architecture Decisions diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d49187c564..a1d1ac3683 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -25,7 +25,7 @@ Use it before writing from scratch. The skill scans recent commits for user-facing changes and drafts doc updates. Run it after landing features, before a release, or to find doc gaps. For example, ask your agent to "catch up the docs for the changes I made in this PR". -During release prep, run the skill first, make doc version bumps, regenerate user skills, then open the docs refresh PR. +During release prep, run the skill first, regenerate user skills, then open the docs refresh PR. The skill lives in `.agents/skills/nemoclaw-contributor-update-docs/` and follows the style guide below automatically. @@ -66,9 +66,8 @@ NemoClaw maintainers refresh the generated user skills once per release during r For daily release prep, the NemoClaw maintainers use this sequence: 1. Run the `nemoclaw-contributor-update-docs` skill for the day's release prep. -2. Make doc version bumps by updating `versions1.json` and `project.json` in the `docs/` directory. -3. Run `python scripts/docs-to-skills.py docs/ .agents/skills/ --prefix nemoclaw-user --doc-platform fern-mdx`. -4. Create the PR with both docs and generated user skills. +2. Run `python scripts/docs-to-skills.py docs/ .agents/skills/ --prefix nemoclaw-user --doc-platform fern-mdx`. +3. Create the PR with both docs and generated user skills. To regenerate skills manually during release prep, run from the repo root: diff --git a/docs/about/release-notes.mdx b/docs/about/release-notes.mdx index 5720b3d2db..2bfaf0d43c 100644 --- a/docs/about/release-notes.mdx +++ b/docs/about/release-notes.mdx @@ -11,13 +11,38 @@ content: --- NVIDIA NemoClaw is available in early preview starting March 16, 2026. Use this page to track changes. +## v0.0.51 + +NemoClaw v0.0.51 improves messaging controls, local inference setup, policy validation, and onboarding recovery: + +- Slack setup now supports channel allowlisting. During onboarding, `channels add slack`, and non-interactive rebuilds, set `SLACK_ALLOWED_CHANNELS` to restrict channel `@mention` handling to selected Slack channel IDs. Combine it with `SLACK_ALLOWED_USERS` when you want both channel and member checks. +- Local Ollama setup now detects host installations that are below the minimum supported version and offers an explicit upgrade path. On macOS, NemoClaw uses Homebrew. On Linux, NemoClaw uses the system installer for upgrades and refuses non-interactive upgrade paths that would require a hidden sudo prompt. +- Managed Ollama model selection now uses a memory-aware registry for starter models. If a known bootstrap model does not fit currently available GPU memory, NemoClaw warns and falls back to the largest known model that does fit instead of starting a model that is likely to fail. +- `nemoclaw resources` and `NEMOCLAW_RESOURCE_PROFILE` expose sandbox CPU and memory profiles. Profiles can be selected during onboarding, and `NEMOCLAW_CPU` or `NEMOCLAW_RAM` can override the selected profile for scripted runs. +- Cloudflare named tunnels are supported through `CLOUDFLARE_TUNNEL_TOKEN`. `nemoclaw tunnel start` passes the token through the environment and expects the named tunnel route to already point at the dashboard port. +- Jira policy validation guidance now matches the maintained preset. Use a Node HTTPS status probe for Atlassian API access and an explicit status-only curl probe for `auth.atlassian.com` when validating approved requests manually. +- Onboarding recovers more cleanly across host and runtime edge cases, including root-owned config sync directories, stale dashboard port allocation, unreachable Docker daemons, stale dashboard forwards, default NVIDIA CDI spec directories, and Linux Docker-driver health checks. + +## v0.0.50 + +NemoClaw v0.0.50 focused on onboarding reliability, local inference hardening, messaging diagnostics, and sandbox lifecycle cleanup: + +- `nemoclaw onboard` handles DGX Spark and DGX Station managed vLLM setup more consistently, including restored vLLM menu entries and CPU-fallback detection on Spark hosts. +- Non-interactive Linux Ollama setup can use a sudo-free user-local install path when passwordless sudo is unavailable. The docs now describe `NEMOCLAW_OLLAMA_INSTALL_MODE`, the user-local install trade-offs, and the manual `zstd` requirement. +- Compatible endpoint setup rejects `host.docker.internal` inference URLs because OpenShell sandboxes do not have a portable host-service route through that name. Use Local Ollama's authenticated proxy path or a policy-managed host service instead. +- Telegram setup now surfaces BotFather group privacy guidance. Disable privacy mode, then remove and re-add the bot to each group before testing group delivery. +- Sandbox logs merge OpenClaw gateway output and OpenShell audit events into one stream, and `--tail` applies once to the merged result so policy denials appear beside gateway logs. +- Maintenance commands recover the OpenShell gateway before retrying sandbox-list operations, which makes rebuild, recover, upgrade, and backup flows more resilient after gateway drift. +- NemoClaw no longer writes proxy hooks into sandbox shell startup files. Local proxy configuration stays on supported OpenShell and NemoClaw paths rather than mutating user shell rc files. +- Windows bootstrap installs Ubuntu 24.04 when WSL is present but no Ubuntu distribution is registered. + ## v0.0.49 NemoClaw v0.0.49 is a hardening release focused on reliability, clearer diagnostics, OpenClaw compatibility, and stronger validation coverage: - Gateway failures now fail faster and explain more. `nemoclaw status` classifies gateway probe failures by layer, distinguishing a named gateway port that is not accepting connections, a named gateway that is present but not Connected, the active OpenShell gateway pointing at a different name, and a named gateway that is not configured at all. `nemoclaw connect` exits early with recovery guidance when the OpenShell gateway is down. - Gateway upgrade and fallback paths are more stable. The release hardens older gateway fallback coverage, OpenShell gateway upgrade checks, crash-loop detection tests, and Brev GPU bridge gateway traffic coverage. -- Status, doctor, and `shields status` now report a fresh mutable sandbox as not configured instead of `down`, and `nemoclaw logs --tail ` is locked in as a NemoClaw line count rather than OpenShell's follow-flag pun. `nemoclaw debug --quick` reports restricted kernel-log access as a skipped section instead of surfacing raw `dmesg` permission errors. +- Status and doctor now report a fresh mutable sandbox as not configured instead of `down`, and `nemoclaw logs --tail ` is locked in as a NemoClaw line count rather than OpenShell's follow-flag pun. `nemoclaw debug --quick` reports restricted kernel-log access as a skipped section instead of surfacing raw `dmesg` permission errors. - OpenClaw compatibility is more resilient across runtime changes. Kimi mixed tool calls are normalized more consistently, compatible OpenClaw JSON envelope changes are tolerated in tests, and OpenClaw patch drift is easier to classify during image builds. - Messaging channel removal is now a clean teardown. The sandbox registry and onboard session policy preset state stay in sync so removed presets do not return during later `onboard --resume` or rebuild flows; QR-paired channels also have their durable in-sandbox session directory wiped before the rebuild and removal aborts cleanly when that wipe cannot be confirmed; and `~/.nemoclaw/config.json` is re-synced from the host across every rebuild resume path so the OpenClaw plugin no longer crashes on the Dockerfile placeholder. - Hermes sandboxes apply only the messaging channel policies the operator selects instead of pre-enabling every Hermes messaging provider, and dynamic preset application resolves Hermes-specific policy content so Discord on Hermes no longer falls back to generic Node allowlists. diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index c6d2da4c17..0000000000 --- a/docs/conf.py +++ /dev/null @@ -1,163 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import json -import logging -import sys -from datetime import date -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent)) -sys.path.insert(0, str(Path(__file__).parent / "_ext")) - -project = "NVIDIA NemoClaw Developer Guide" -this_year = date.today().year -copyright = f"{this_year}, NVIDIA Corporation" -author = "NVIDIA Corporation" - -# Read the preferred version from versions1.json so the version switcher -# can match it. versions1.json is the source of truth for the switcher -# dropdown; reading from it keeps conf.py in sync automatically. -_versions = json.loads((Path(__file__).parent / "versions1.json").read_text()) -_preferred = [v["version"] for v in _versions if v.get("preferred")] -assert len(_preferred) == 1, ( - f"docs/versions1.json must have exactly one entry with preferred: true; found {len(_preferred)}" -) -release = _preferred[0] - - -extensions = [ - "myst_parser", - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.napoleon", - "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", - "sphinx_copybutton", - "sphinx_design", - "sphinxcontrib.mermaid", - "json_output", - "search_assets", - "sphinx_reredirects", -] - -redirects = { - "reference/inference-profiles": "../inference/inference-options.html", - # Get Started reorganization (April 2026): the Windows pre-setup page - # moved out of its earlier locations and is now get-started/ - # windows-preparation.html. The short-lived platform-setup hub and - # tutorials/dgx-spark pages were removed; DGX Spark content now lives - # in the NVIDIA Spark playbook (https://build.nvidia.com/spark/nemoclaw). - "get-started/windows-setup": "windows-preparation.html", - # Manage Sandboxes reorganization (May 2026): operational pages moved - # from deployment/ and workspace/ into manage-sandboxes/. - "deployment/set-up-telegram-bridge": "../manage-sandboxes/messaging-channels.html", - "workspace/workspace-files": "../manage-sandboxes/workspace-files.html", - "workspace/backup-restore": "../manage-sandboxes/backup-restore.html", -} - -# sphinx-reredirects rewrites redirect files on every incremental build and -# logs each rewrite at info level. Keep real redirect warnings visible. -logging.getLogger("sphinx.sphinx_reredirects").setLevel(logging.WARNING) - -autodoc_default_options = { - "members": True, - "undoc-members": False, - "show-inheritance": True, - "member-order": "bysource", -} -autodoc_typehints = "description" -autodoc_class_signature = "separated" - -copybutton_exclude = ".linenos, .gp, .go" - -exclude_patterns = [ - "README.md", - "SETUP.md", - "CONTRIBUTING.md", - "_build/**", - "_ext/**", -] - -suppress_warnings = ["myst.header"] - -myst_linkify_fuzzy_links = False -myst_heading_anchors = 4 -myst_enable_extensions = [ - "colon_fence", - "deflist", - "dollarmath", - "fieldlist", - "substitution", -] -myst_links_external_new_tab = True - -myst_substitutions = { - "version": release, -} - -templates_path = ["_templates"] - -html_theme = "nvidia_sphinx_theme" -html_copy_source = False -html_show_sourcelink = False -html_show_sphinx = False - -mermaid_init_js = ( - "mermaid.initialize({" - " startOnLoad: true," - " theme: 'base'," - " themeVariables: {" - " background: '#ffffff'," - " primaryColor: '#76b900'," - " primaryTextColor: '#000000'," - " primaryBorderColor: '#000000'," - " lineColor: '#000000'," - " textColor: '#000000'," - " mainBkg: '#ffffff'," - " nodeBorder: '#000000'" - " }" - "});" -) - -html_domain_indices = False -html_use_index = False -html_extra_path = ["project.json", "versions1.json"] -highlight_language = "console" - -html_theme_options = { - # "public_docs_features": True, # TODO: Uncomment this when the docs are public - "announcement": ( - "🔔 NVIDIA NemoClaw is alpha software. APIs and behavior" - " may change without notice. Do not use in production." - ), - "switcher": { - "json_url": "../versions1.json", - "version_match": release, - }, - "icon_links": [ - { - "name": "NemoClaw GitHub", - "url": "https://github.com/NVIDIA/NemoClaw", - "icon": "fa-brands fa-github", - "type": "fontawesome", - }, - { - "name": "NemoClaw Discord", - "url": "https://discord.gg/XFpfPv9Uvx", - "icon": "fa-brands fa-discord", - "type": "fontawesome", - }, - ], -} - -html_baseurl = "https://docs.nvidia.com/nemoclaw/latest/" - -# Keep project.json in sync with the resolved release version so the -# static copy served alongside the docs always reports the correct version. -# Write only when the contents change so sphinx-autobuild does not detect -# a self-induced source change and rebuild in an infinite loop. -_project_json = Path(__file__).parent / "project.json" -_project_json_contents = json.dumps({"name": "nemoclaw", "version": release}) + "\n" -if not _project_json.exists() or _project_json.read_text() != _project_json_contents: - _project_json.write_text(_project_json_contents) diff --git a/docs/project.json b/docs/project.json deleted file mode 100644 index 2f76877d4c..0000000000 --- a/docs/project.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "nemoclaw", "version": "0.0.49"} diff --git a/docs/versions1.json b/docs/versions1.json deleted file mode 100644 index 5308b67ab1..0000000000 --- a/docs/versions1.json +++ /dev/null @@ -1,95 +0,0 @@ -[ - { - "preferred": true, - "version": "0.0.49", - "url": "https://docs.nvidia.com/nemoclaw/0.0.49/" - }, - { - "version": "0.0.46", - "url": "https://docs.nvidia.com/nemoclaw/0.0.46/" - }, - { - "version": "0.0.45", - "url": "https://docs.nvidia.com/nemoclaw/0.0.45/" - }, - { - "version": "0.0.44", - "url": "https://docs.nvidia.com/nemoclaw/0.0.44/" - }, - { - "version": "0.0.43", - "url": "https://docs.nvidia.com/nemoclaw/0.0.43/" - }, - { - "version": "0.0.42", - "url": "https://docs.nvidia.com/nemoclaw/0.0.42/" - }, - { - "version": "0.0.41", - "url": "https://docs.nvidia.com/nemoclaw/0.0.41/" - }, - { - "version": "0.0.40", - "url": "https://docs.nvidia.com/nemoclaw/0.0.40/" - }, - { - "version": "0.0.39", - "url": "https://docs.nvidia.com/nemoclaw/0.0.39/" - }, - { - "version": "0.0.38", - "url": "https://docs.nvidia.com/nemoclaw/0.0.38/" - }, - { - "version": "0.0.37", - "url": "https://docs.nvidia.com/nemoclaw/0.0.37/" - }, - { - "version": "0.0.36", - "url": "https://docs.nvidia.com/nemoclaw/0.0.36/" - }, - { - "version": "0.0.35", - "url": "https://docs.nvidia.com/nemoclaw/0.0.35/" - }, - { - "version": "0.0.34", - "url": "https://docs.nvidia.com/nemoclaw/0.0.34/" - }, - { - "version": "0.0.33", - "url": "https://docs.nvidia.com/nemoclaw/0.0.33/" - }, - { - "version": "0.0.32", - "url": "https://docs.nvidia.com/nemoclaw/0.0.32/" - }, - { - "version": "0.0.31", - "url": "https://docs.nvidia.com/nemoclaw/0.0.31/" - }, - { - "version": "0.0.25", - "url": "https://docs.nvidia.com/nemoclaw/0.0.25/" - }, - { - "version": "0.0.24", - "url": "https://docs.nvidia.com/nemoclaw/0.0.24/" - }, - { - "version": "0.0.23", - "url": "https://docs.nvidia.com/nemoclaw/0.0.23/" - }, - { - "version": "0.0.22", - "url": "https://docs.nvidia.com/nemoclaw/0.0.22/" - }, - { - "version": "0.0.21", - "url": "https://docs.nvidia.com/nemoclaw/0.0.21/" - }, - { - "version": "0.0.20", - "url": "https://docs.nvidia.com/nemoclaw/0.0.20/" - } -] diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts index d7ba354102..3f9da1b7d9 100644 --- a/scripts/bump-version.ts +++ b/scripts/bump-version.ts @@ -28,12 +28,6 @@ type BlueprintManifest = { version?: string; }; -type DocsVersionEntry = { - preferred?: boolean; - version: string; - url: string; -}; - function parseJson(text: string): T { return JSON.parse(text); } @@ -42,10 +36,6 @@ function parseYaml(text: string): T { return YAML.parse(text); } -function isErrnoException(error: unknown): error is NodeJS.ErrnoException { - return typeof error === "object" && error !== null && "code" in error; -} - function readStringProperty(value: object | null, key: string): string | undefined { if (!value || Array.isArray(value)) { return undefined; @@ -66,7 +56,6 @@ const REPO_ROOT = process.cwd(); const ROOT_PACKAGE_JSON = path.join(REPO_ROOT, "package.json"); const PLUGIN_PACKAGE_JSON = path.join(REPO_ROOT, "nemoclaw", "package.json"); const BLUEPRINT_YAML = path.join(REPO_ROOT, "nemoclaw-blueprint", "blueprint.yaml"); -const DOCS_VERSIONS_JSON = path.join(REPO_ROOT, "docs", "versions1.json"); const INSTALL_SH = path.join(REPO_ROOT, "scripts", "install.sh"); const README_MD = path.join(REPO_ROOT, "README.md"); const QUICKSTART_MDX = path.join(REPO_ROOT, "docs", "get-started", "quickstart.mdx"); @@ -75,9 +64,6 @@ const FILES_TO_STAGE = [ ROOT_PACKAGE_JSON, PLUGIN_PACKAGE_JSON, BLUEPRINT_YAML, - // DOCS_CONF reads the version dynamically from DOCS_VERSIONS_JSON at build time. - // DOCS_PROJECT_JSON is regenerated by conf.py at build time. - DOCS_VERSIONS_JSON, INSTALL_SH, ...VERSIONED_DOC_LINK_FILES, ]; @@ -112,9 +98,6 @@ function main(): void { updatePackageJson(PLUGIN_PACKAGE_JSON, options.version); updateBlueprintVersion(options.version); updateInstallScriptDefaultVersion(previousVersion, options.version); - // conf.py reads the version dynamically from versions1.json at build time. - // project.json is regenerated by conf.py at build time. - updateDocsVersionsJson(options.version); updateDocsVersionLinks(nextDocsPublicUrl); updateInstallAndUninstallDocs(nextDocsVersion); @@ -340,54 +323,6 @@ function updateInstallScriptDefaultVersion(previousVersion: string, nextVersion: ); } -function updateDocsVersionsJson(version: string): void { - const currentEntries = readDocsVersionsJson(); - const filteredEntries = currentEntries.filter((entry) => entry.version !== version); - const nextEntries: DocsVersionEntry[] = [ - { - preferred: true, - version, - url: buildDocsVersionUrl(version), - }, - ...filteredEntries.map((entry) => ({ - version: entry.version, - url: buildDocsVersionUrl(entry.version), - })), - ].slice(0, 10); - - writeFileSync(DOCS_VERSIONS_JSON, `${JSON.stringify(nextEntries, null, 2)}\n`, "utf8"); -} - -function readDocsVersionsJson(): DocsVersionEntry[] { - try { - const parsed = parseJson>>(readText(DOCS_VERSIONS_JSON)); - if (!Array.isArray(parsed)) { - throw new Error("docs/versions1.json must contain an array"); - } - return parsed.map((entry) => { - const version = typeof entry.version === "string" ? entry.version : undefined; - if (!version) { - throw new Error("Each docs/versions1.json entry must include a string version"); - } - const url = typeof entry.url === "string" ? entry.url : undefined; - return { - preferred: entry.preferred === true ? true : undefined, - version, - url: url && url.length > 0 ? url : buildDocsVersionUrl(version), - }; - }); - } catch (error) { - if (isErrnoException(error) && error.code === "ENOENT") { - return []; - } - throw error; - } -} - -function buildDocsVersionUrl(version: string): string { - return `https://docs.nvidia.com/nemoclaw/${version}/`; -} - function updateDocsVersionLinks(nextDocsPublicUrl: string): void { for (const filePath of VERSIONED_DOC_LINK_FILES) { const current = readText(filePath); @@ -467,9 +402,6 @@ function verifyVersionState( assertEqual(blueprint.version, version, "blueprint version mismatch"); requireContains(INSTALL_SH, `DEFAULT_NEMOCLAW_VERSION="${version}"`); - // conf.py reads the version dynamically from versions1.json; project.json - // is regenerated at build time. Only versions1.json needs verification. - verifyDocsVersionsJson(version); requireContains(README_MD, docsPublicUrl); requireContains(README_MD, docsDisplayVersion); requireContains(QUICKSTART_MDX, docsDisplayVersion); @@ -588,7 +520,7 @@ function buildPrBody(previousVersion: string, nextVersion: string, options: PrBo "## Testing", `- [${options.ranFormat ? "x" : " "}] \`npx prek run --all-files\` passes (or equivalently \`make check\`).`, `- [${options.ranTests ? "x" : " "}] \`npm test\` passes.`, - "- [ ] `make docs` builds without warnings. (for doc-only changes)", + "- [ ] `npm run docs` builds without warnings. (for doc-only changes)", "", "## Checklist", "", @@ -650,43 +582,6 @@ function requireContains(filePath: string, text: string): void { } } -function verifyDocsVersionsJson(expectedNewestVersion: string): void { - const entries = readJson(DOCS_VERSIONS_JSON); - if (!Array.isArray(entries)) { - throw new Error("docs/versions1.json must contain an array"); - } - if (entries.length === 0) { - throw new Error("docs/versions1.json must contain at least one version entry"); - } - if (entries.length > 5) { - throw new Error(`docs/versions1.json must contain at most 5 entries; found ${entries.length}`); - } - - entries.forEach((entry, index) => { - if (typeof entry.version !== "string" || entry.version.length === 0) { - throw new Error(`docs/versions1.json entry ${index} is missing a valid version`); - } - const expectedUrl = buildDocsVersionUrl(entry.version); - if (entry.url !== expectedUrl) { - throw new Error( - `docs/versions1.json entry ${index} has url '${entry.url}', expected '${expectedUrl}'`, - ); - } - if (index === 0) { - if (entry.version !== expectedNewestVersion) { - throw new Error( - `docs/versions1.json first entry must be ${expectedNewestVersion}; found ${entry.version}`, - ); - } - if (entry.preferred !== true) { - throw new Error("docs/versions1.json first entry must have preferred: true"); - } - } else if ("preferred" in entry && entry.preferred !== undefined) { - throw new Error("Only the first docs/versions1.json entry may set preferred"); - } - }); -} - function verifyDocsLinks(filePath: string, expectedDocsPublicUrl: string): void { const content = readText(filePath); const matches = Array.from( diff --git a/scripts/docs-to-skills.py b/scripts/docs-to-skills.py index 9a874ccd17..494463a540 100755 --- a/scripts/docs-to-skills.py +++ b/scripts/docs-to-skills.py @@ -60,11 +60,17 @@ import json import os import re +import shutil import sys import textwrap from dataclasses import dataclass, field from pathlib import Path +# Image asset extensions that the rewriter copies alongside the +# generated skill file. Local copies keep skills self-contained so they +# render even when the docs site is offline or unpublished. +IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".svg", ".gif", ".webp"}) + def load_html_baseurl(docs_dir: Path) -> str | None: """Read ``html_baseurl`` from a Sphinx ``conf.py`` without executing it. @@ -742,7 +748,7 @@ def rewrite_doc_paths( doc_to_skill: dict[str, str], html_baseurl: str | None = None, doc_platform: str = "myst-md", -) -> str: +) -> tuple[str, list[tuple[Path, str]]]: """Resolve relative doc paths to skill cross-refs or published URLs. Skill files are meant to be self-contained, so the rewriter never @@ -752,22 +758,35 @@ def rewrite_doc_paths( 1. If the target is an external URL, an anchor, or a ``mailto:`` reference, or the target is not a recognized doc link for the selected platform, leave it untouched. - 2. If the target resolves to a doc that has a generated skill, + 2. If the target is an image asset that exists under ``docs/``, + record a copy task and rewrite the link to ``images/``. + The caller is responsible for copying the recorded files into the + skill output directory after writing the markdown body. + 3. If the target resolves to a doc that has a generated skill, replace the whole link with ``text (use the `` skill)``. - 3. If the target is a page inside ``docs/``, emit + 4. If the target is a page inside ``docs/``, emit ``[text](.html)`` using the base URL read from ``conf.py``. - 4. Otherwise (target outside ``docs/``, or no base URL available), + 5. Otherwise (target outside ``docs/``, or no base URL available), strip the hyperlink and keep the link text. Self-containment wins over navigability in the fallback. Include placeholders that referenced ``docs/``-relative paths are rewritten the same way: published URL if available, else dropped. + + Returns the rewritten text plus the list of ``(source_path, basename)`` + image-copy tasks recorded during rewriting. """ repo_root = docs_dir.parent source_dir = source_page.path.parent doc_extension = DOC_EXTENSIONS.get(doc_platform, ".md") + image_copies: list[tuple[Path, str]] = [] + + def _record_image_copy(resolved: Path) -> str: + """Record an image-copy task and return the link target for it.""" + image_copies.append((resolved, resolved.name)) + return f"images/{resolved.name}" def _to_html_url(resolved: Path, frag: str) -> str | None: """Published URL for a doc under ``docs/``; ``None`` otherwise.""" @@ -829,6 +848,20 @@ def _resolve_link(match: re.Match) -> str: if not candidates: return match.group(0) + # Image assets that exist under docs/ are copied alongside the + # skill file so the rendered link works offline. Fragments are + # meaningless on local images, so they are dropped. + for resolved in candidates: + if resolved.suffix.lower() not in IMAGE_EXTENSIONS: + continue + try: + resolved.relative_to(docs_dir) + except ValueError: + continue + if not resolved.is_file(): + continue + return f"[{link_text}]({_record_image_copy(resolved)})" + # Check if target doc maps to a generated skill for resolved in candidates: try: @@ -869,7 +902,7 @@ def _resolve_include(match: re.Match) -> str: text, ) - return text + return text, image_copies def extract_related_skills(text: str) -> tuple[str, list[str]]: @@ -1460,18 +1493,23 @@ def generate_skill( inter-doc links are rewritten to either skill cross-references or absolute HTTPS URLs (see :func:`rewrite_doc_paths`), the emitted content is independent of where it is written and can safely be - mirrored across multiple output roots. + mirrored across multiple output roots. Image assets referenced by + the source pages are copied alongside the file that links them so + the rendered skill works without network access. Returns a summary dict for reporting. """ - def _clean(text: str, source: DocPage) -> str: + skill_md_images: list[tuple[Path, str]] = [] + ref_images: dict[str, list[tuple[Path, str]]] = {} + + def _clean(text: str, source: DocPage, image_acc: list[tuple[Path, str]]) -> str: """Apply directive cleanup and path rewriting for a source page.""" if doc_platform == "fern-mdx": result = clean_fern_mdx(text) else: result = clean_myst_directives(text) if docs_dir and doc_to_skill is not None: - result = rewrite_doc_paths( + result, copies = rewrite_doc_paths( result, source, docs_dir, @@ -1479,6 +1517,7 @@ def _clean(text: str, source: DocPage) -> str: html_baseurl=html_baseurl, doc_platform=doc_platform, ) + image_acc.extend(copies) return result procedures, deferred_procedures, context_pages, reference_pages = ( @@ -1532,7 +1571,7 @@ def _clean(text: str, source: DocPage) -> str: for pp in procedures: for heading, content in pp.sections: if heading.lower() in ("prerequisites", "before you begin"): - cleaned = _clean(content, pp) + cleaned = _clean(content, pp, skill_md_images) for item_line in cleaned.split("\n"): stripped = item_line.strip() if stripped.startswith("- "): @@ -1566,17 +1605,17 @@ def _clean(text: str, source: DocPage) -> str: if heading.lower() in skip_sections: continue if heading.lower() in related_sections: - collected_related.append(_clean(content, pp)) + collected_related.append(_clean(content, pp, skill_md_images)) continue if not heading: - cleaned = _clean(content, pp) + cleaned = _clean(content, pp, skill_md_images) cleaned = re.sub(r"^#\s+.+\n+", "", cleaned) if cleaned.strip(): lines.append(cleaned) lines.append("") continue - cleaned_content = _clean(content, pp) + cleaned_content = _clean(content, pp, skill_md_images) lines.append(f"## {heading}") lines.append("") lines.append(cleaned_content) @@ -1643,13 +1682,15 @@ def _clean(text: str, source: DocPage) -> str: ref_files: dict[str, str] = {} for rp in deferred_procedures + reference_pages + context_pages: ref_name = rp.path.stem + ".md" - body = _clean(rp.body, rp) + ref_image_acc: list[tuple[Path, str]] = [] + body = _clean(rp.body, rp, ref_image_acc) if doc_platform == "myst-md" and rp.title: body = canonicalize_leading_h1(body, rp.title) elif doc_platform == "fern-mdx" and rp.title and not body.startswith("# "): body = f"# {rp.title}\n\n{body}".rstrip() body = normalize_heading_levels(body) ref_files[ref_name] = body + ref_images[ref_name] = ref_image_acc # --- Write output --- summary = { @@ -1670,6 +1711,7 @@ def _clean(text: str, source: DocPage) -> str: (skill_dir / "SKILL.md").write_text( skill_md.rstrip("\n") + "\n", encoding="utf-8" ) + _copy_skill_images(skill_dir, skill_md_images) spdx_ref = markdown_spdx_header() @@ -1681,10 +1723,38 @@ def _clean(text: str, source: DocPage) -> str: (refs_dir / fname).write_text( spdx_ref + content.rstrip("\n") + "\n", encoding="utf-8" ) + _copy_skill_images(refs_dir, ref_images.get(fname, [])) return summary +def _copy_skill_images(target_dir: Path, copies: list[tuple[Path, str]]) -> None: + """Copy recorded image assets next to the skill file that references them. + + ``target_dir`` is the directory containing the markdown file that + references the images (e.g. the skill root for ``SKILL.md`` or the + ``references/`` directory for sibling reference files). Images land + in ``target_dir / "images" / basename`` so the rewritten link + ``images/`` resolves correctly. + """ + if not copies: + return + images_dir = target_dir / "images" + images_dir.mkdir(parents=True, exist_ok=True) + seen: set[str] = set() + for src, basename in copies: + if basename in seen: + continue + seen.add(basename) + dest = images_dir / basename + try: + if dest.exists() and dest.read_bytes() == src.read_bytes(): + continue + shutil.copyfile(src, dest) + except OSError as exc: + print(f" warning: failed to copy {src} -> {dest}: {exc}", file=sys.stderr) + + # --------------------------------------------------------------------------- # Grouping strategies # --------------------------------------------------------------------------- @@ -1938,17 +2008,20 @@ def main(): pass # Published-URL fallback for inter-doc links that do not map to a - # generated skill. Read from Sphinx's conf.py so the script stays - # project-agnostic — any docs tree with an html_baseurl assignment - # will just work. - html_baseurl = load_html_baseurl(docs_dir_resolved) - if html_baseurl is None: - print( - f" warning: no html_baseurl found in {docs_dir_resolved}/conf.py; " - "inter-doc links without a skill mapping will be stripped to plain " - "text to keep skills self-contained.", - file=sys.stderr, - ) + # generated skill. Only the legacy MyST/Sphinx path uses ``conf.py`` + # for ``html_baseurl``; Fern docs copy assets locally instead and + # have no equivalent base URL to load. + if args.doc_platform == "myst-md": + html_baseurl = load_html_baseurl(docs_dir_resolved) + if html_baseurl is None: + print( + f" warning: no html_baseurl found in {docs_dir_resolved}/conf.py; " + "inter-doc links without a skill mapping will be stripped to plain " + "text to keep skills self-contained.", + file=sys.stderr, + ) + else: + html_baseurl = None # Generate skills dirs_str = ", ".join(str(d) for d in args.output_dirs)