Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/nemoclaw-get-started/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
| Dependency | Version |
|------------|----------------------------------|
| Linux | Ubuntu 22.04 LTS or later |
| Node.js | 20 or later |
| Node.js | 22.16 or later |
| npm | 10 or later |
| Container runtime | Supported runtime installed and running |
| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ If you see an unsupported platform error, verify that you are running on a suppo

### Node.js version is too old

NemoClaw requires Node.js 20 or later.
NemoClaw requires Node.js 22.16 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 the version is below 22.16, install a supported release.
If you use nvm, run:

```console
$ nvm install 20
$ nvm use 20
$ nvm install 22
$ nvm use 22
```

Then re-run the installer.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
run: npx vitest run --coverage

- name: Check coverage ratchet
run: bash scripts/check-coverage-ratchet.sh
run: npx tsx scripts/check-coverage-ratchet.ts

build-sandbox-images:
runs-on: ubuntu-latest
Expand Down
12 changes: 11 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ repos:
name: SPDX license headers (insert if missing)
entry: bash scripts/check-spdx-headers.sh --fix
language: system
files: ^(nemoclaw/src/.*\.ts|nemoclaw-blueprint/.*\.py|.*\.sh)$
files: ^(nemoclaw/src/.*\.ts|scripts/.*\.ts|nemoclaw-blueprint/.*\.py|.*\.sh)$
exclude: ^nemoclaw-blueprint/.*__init__\.py$
pass_filenames: true
priority: 4
Expand Down Expand Up @@ -206,6 +206,16 @@ repos:
stages: [pre-push]
priority: 10

- id: tsc-check-cli
name: TypeScript type check (CLI)
entry: npx tsc -p tsconfig.cli.json
language: system
pass_filenames: false
files: ^(bin|scripts)/
types_or: [ts, tsx]
stages: [pre-push]
priority: 10

default_language_version:
python: python3

Expand Down
11 changes: 9 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Open an issue when you encounter one of the following situations.

Install the following before you begin.

- Node.js 20+ and npm 10+
- Node.js 22.16+ and npm 10+
- Python 3.11+ (for blueprint and documentation builds)
- Docker (running)
- [uv](https://docs.astral.sh/uv/) (for Python dependency management)
Expand Down Expand Up @@ -45,6 +45,12 @@ npm run build # one-time compile
npm run dev # watch mode
```

The CLI (`bin/`, `scripts/`) is type-checked separately:

```bash
npm run typecheck:cli # or: npx tsc -p tsconfig.cli.json
```

## Main Tasks

These are the primary `make` and `npm` targets for day-to-day development:
Expand All @@ -54,6 +60,7 @@ These are the primary `make` and `npm` targets for day-to-day development:
| `make check` | Run all linters (TypeScript + Python) |
| `make lint` | Same as `make check` |
| `make format` | Auto-format TypeScript and Python source |
| `npm run typecheck:cli` | Type-check CLI TypeScript (`bin/`, `scripts/`) |
| `npm test` | Run root-level tests (`test/*.test.js`) |
| `cd nemoclaw && npm test` | Run plugin unit tests (Vitest) |
| `make docs` | Build documentation (Sphinx/MyST) |
Expand All @@ -68,7 +75,7 @@ All git hooks are managed by [prek](https://prek.j178.dev/), a fast, single-bina
|------|-----------|
| **pre-commit** | File fixers, formatters, linters, Vitest (plugin) |
| **commit-msg** | commitlint (Conventional Commits) |
| **pre-push** | TypeScript type check (`tsc --noEmit`), Pyright (Python) |
| **pre-push** | TypeScript type check (`tsc --noEmit` for plugin, JS, and CLI) |

For a full manual check: `npx prek run --all-files`. For scoped runs: `npx prek run --from-ref <base> --to-ref HEAD`.

Expand Down
21 changes: 5 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
# Build args for config that varies per deployment.
# nemoclaw onboard passes these at image build time.
ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b
ARG NEMOCLAW_PROVIDER_KEY=nvidia

Check warning on line 54 in Dockerfile

View workflow job for this annotation

GitHub Actions / build-and-push

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_PROVIDER_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b
ARG CHAT_UI_URL=http://127.0.0.1:18789
ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1
Expand All @@ -64,7 +64,7 @@
# SECURITY: Promote build-args to env vars so the Python script reads them
# via os.environ, never via string interpolation into Python source code.
# Direct ARG interpolation into python3 -c is a code injection vector (C-2).
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \

Check warning on line 67 in Dockerfile

View workflow job for this annotation

GitHub Actions / build-and-push

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_PROVIDER_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
NEMOCLAW_PROVIDER_KEY=${NEMOCLAW_PROVIDER_KEY} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
CHAT_UI_URL=${CHAT_UI_URL} \
Expand Down Expand Up @@ -124,21 +124,6 @@
RUN openclaw doctor --fix > /dev/null 2>&1 || true \
&& openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true

<<<<<<< HEAD
# Save build-time config as defaults — startup script copies to writable HOME
USER root
RUN cp -a /sandbox/.openclaw /opt/nemoclaw-defaults \
&& cp -a /sandbox/.nemoclaw /opt/nemoclaw-defaults/.nemoclaw
USER sandbox

# At runtime, HOME=/data (writable volume mount from FleetManager).
# ReadonlyRootfs makes /sandbox read-only, so all writes go to /data.
ENV HOME=/data

EXPOSE 3100

ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
=======
# Lock openclaw.json via DAC: chown to root so the sandbox user cannot modify
# it at runtime. This works regardless of Landlock enforcement status.
# The Landlock policy (/sandbox/.openclaw in read_only) provides defense-in-depth
Expand All @@ -163,8 +148,12 @@
&& chmod 444 /sandbox/.openclaw/.config-hash \
&& chown root:root /sandbox/.openclaw/.config-hash

# WOPR sidecar — ensure writable HOME for runtime state
ENV HOME=/data
Comment on lines +151 to +152

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unprovisioned /data home 🐞 Bug ⛯ Reliability

Dockerfile sets HOME=/data, but the image never creates /data or makes it writable for the
sandbox/gateway users; the entrypoint writes under ~/.openclaw as the sandbox user, which will fail
if /data isn’t mounted and writable. This can prevent the container from starting (set -e) in
environments that don’t pre-create a writable /data volume.
Agent Prompt
### Issue description
The container sets `HOME=/data` but does not ensure `/data` exists and is writable for the `sandbox`/`gateway` users. Since the entrypoint writes under `~/.openclaw/...` as `sandbox`, startup can fail when `/data` isn’t mounted or is root-owned.

### Issue Context
- `Dockerfile` sets `ENV HOME=/data`.
- `scripts/nemoclaw-start.sh` writes auth profiles to `~/.openclaw/...` via `os.path.expanduser('~')` as the `sandbox` user.
- The base image provisions `/sandbox` but not `/data`.

### Fix Focus Areas
- Dockerfile[151-155]
- scripts/nemoclaw-start.sh[87-107]
- Dockerfile.base[82-89]

### Suggested fix
In `Dockerfile`, before setting `ENV HOME=/data`, add a step to create `/data` and make it writable for both `gateway` and `sandbox` (e.g., `mkdir -p /data && chmod 1777 /data`, or a tighter ownership/ACL strategy that still allows both users to write).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


EXPOSE 3100

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
CMD ["/bin/bash"]
>>>>>>> upstream/main
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
| Dependency | Version |
|------------|----------------------------------|
| Linux | Ubuntu 22.04 LTS or later |
| Node.js | 20 or later |
| Node.js | 22.16 or later |
| npm | 10 or later |
| Container runtime | Supported runtime installed and running |
| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed |
Expand Down Expand Up @@ -222,6 +222,20 @@ Credentials stay on the host in `~/.nemoclaw/credentials.json`. The sandbox only

Local Ollama is supported in the standard onboarding flow. Local vLLM remains experimental, and local host-routed inference on macOS still depends on OpenShell host-routing support in addition to the local service itself being reachable on the host.

## Host-Side State and Config

NemoClaw keeps its operator-facing state on the host rather than inside the sandbox.
These are the main files new users usually need to locate:

| Path | Purpose |
|---|---|
| `~/.nemoclaw/credentials.json` | Provider credentials saved during onboarding |
| `~/.nemoclaw/sandboxes.json` | Registered sandbox metadata, including the default sandbox selection |
| `~/.openclaw/openclaw.json` | Host OpenClaw configuration that NemoClaw snapshots or restores during migration flows |

Common environment variables for optional services and local access include `TELEGRAM_BOT_TOKEN`, `ALLOWED_CHAT_IDS`, and `CHAT_UI_URL`.
For normal sandbox setup and reconfiguration, prefer `nemoclaw onboard` over editing these files by hand.

---

## Protection Layers
Expand Down
4 changes: 4 additions & 0 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,7 @@ function getNonInteractiveModel(providerKey) {

// ── Step 1: Preflight ────────────────────────────────────────────

// eslint-disable-next-line complexity
async function preflight() {
step(1, 7, "Preflight checks");

Expand Down Expand Up @@ -1592,6 +1593,7 @@ async function createSandbox(gpu, model, provider, preferredInferenceApi = null)

// ── Step 4: NIM ──────────────────────────────────────────────────

// eslint-disable-next-line complexity
async function setupNim(gpu) {
step(2, 7, "Configuring inference (NIM)");

Expand Down Expand Up @@ -1990,6 +1992,7 @@ async function setupNim(gpu) {

// ── Step 5: Inference provider ───────────────────────────────────

// eslint-disable-next-line complexity
async function setupInference(sandboxName, model, provider, endpointUrl = null, credentialEnv = null) {
step(4, 7, "Setting up inference provider");
runOpenshell(["gateway", "select", GATEWAY_NAME], { ignoreError: true });
Expand Down Expand Up @@ -2073,6 +2076,7 @@ async function setupOpenclaw(sandboxName, model, provider) {

// ── Step 7: Policy presets ───────────────────────────────────────

// eslint-disable-next-line complexity
async function setupPolicies(sandboxName) {
step(7, 7, "Policy presets");

Expand Down
1 change: 1 addition & 0 deletions bin/lib/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function buildPolicyGetCommand(sandboxName) {
return `${getOpenshellCommand()} policy get --full ${shellQuote(sandboxName)} 2>/dev/null`;
}

// eslint-disable-next-line complexity
function applyPreset(sandboxName, presetName) {
// Guard against truncated sandbox names — WSL can truncate hyphenated
// names during argument parsing, e.g. "my-assistant" → "m"
Expand Down
4 changes: 4 additions & 0 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ function printGatewayLifecycleHint(output = "", sandboxName = "", writer = conso
}
}

// eslint-disable-next-line complexity
async function getReconciledSandboxGatewayState(sandboxName) {
let lookup = getSandboxGatewayState(sandboxName);
if (lookup.state === "present") {
Expand Down Expand Up @@ -360,6 +361,7 @@ async function setupSpark() {
run(`sudo bash "${SCRIPTS}/setup-spark.sh"`);
}

// eslint-disable-next-line complexity
async function deploy(instanceName) {
if (!instanceName) {
console.error(" Usage: nemoclaw deploy <instance-name>");
Expand Down Expand Up @@ -569,6 +571,7 @@ async function sandboxConnect(sandboxName) {
exitWithSpawnResult(result);
}

// eslint-disable-next-line complexity
async function sandboxStatus(sandboxName) {
const sb = registry.getSandbox(sandboxName);
const live = parseGatewayInference(
Expand Down Expand Up @@ -757,6 +760,7 @@ function help() {

const [cmd, ...args] = process.argv.slice(2);

// eslint-disable-next-line complexity
(async () => {
// No command → help
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,19 @@ If you see an unsupported platform error, verify that you are running on a suppo

### Node.js version is too old

NemoClaw requires Node.js 20 or later.
NemoClaw requires Node.js 22.16 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 the version is below 22.16, install a supported release.
If you use nvm, run:

```console
$ nvm install 20
$ nvm use 20
$ nvm install 22
$ nvm use 22
```

Then re-run the installer.
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export default [
rules: {
...js.configs.recommended.rules,
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }],
// Cyclomatic complexity — ratchet down to 15 as we refactor suppressed functions
"complexity": ["error", { max: 20 }],
},
},

Expand Down
14 changes: 8 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,17 @@ spin() {

command_exists() { command -v "$1" &>/dev/null; }

MIN_NODE_MAJOR=20
MIN_NODE_VERSION="22.16.0"
MIN_NPM_MAJOR=10
RECOMMENDED_NODE_MAJOR=22
RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_MAJOR} and npm >=${MIN_NPM_MAJOR} (recommended Node.js ${RECOMMENDED_NODE_MAJOR})."
RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_VERSION} and npm >=${MIN_NPM_MAJOR}."
Comment on lines +216 to +218

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This now breaks repo-checkout installs on machines that already have an older Node.js.

main() still calls install_nodejs() first, but that function returns as soon as any node binary exists. With the floor raised to 22.16.0, a workstation on v20.x or v22.14.x now hard-fails here instead of being upgraded, while scripts/install.sh already handles the upgrade path.

Suggested alignment with the curl-pipe installer
 install_nodejs() {
-  if command_exists node; then
-    info "Node.js found: $(node --version)"
-    return
-  fi
+  local node_version="" npm_version="" npm_major=""
+  if command_exists node; then
+    node_version="$(node --version 2>/dev/null || true)"
+    npm_version="$(npm --version 2>/dev/null || true)"
+    npm_major="$(version_major "$npm_version")"
+    info "Node.js found: ${node_version:-unknown}"
+    if version_gte "${node_version#v}" "$MIN_NODE_VERSION" \
+      && [[ "$npm_major" =~ ^[0-9]+$ ]] \
+      && (( npm_major >= MIN_NPM_MAJOR )); then
+      return
+    fi
+    info "Upgrading runtime to satisfy ${RUNTIME_REQUIREMENT_MSG}"
+  fi
 
-  info "Node.js not found — installing via nvm…"
+  info "Installing Node.js via nvm…"

Also applies to: 305-306

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 216 - 218, The script sets
MIN_NODE_VERSION="22.16.0" but install_nodejs() only checks for the existence of
a node binary and returns early, causing machines with older Node (e.g., v20.x
or v22.14.x) to skip upgrade; update install_nodejs() to detect the installed
node version (e.g., run node --version), semver-compare it with
MIN_NODE_VERSION, and only return early if the installed version >=
MIN_NODE_VERSION; otherwise invoke the upgrade/installer path (reuse the
existing logic from scripts/install.sh or the curl-pipe installer) and ensure
RUNTIME_REQUIREMENT_MSG and MIN_NPM_MAJOR checks are enforced after upgrading;
reference install_nodejs(), main(), MIN_NODE_VERSION, MIN_NPM_MAJOR when making
the change.

NEMOCLAW_SHIM_DIR="${HOME}/.local/bin"
ORIGINAL_PATH="${PATH:-}"

# Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2.
# Rejects prerelease suffixes (e.g. "22.16.0-rc.1") to avoid arithmetic errors.
version_gte() {
[[ "$1" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || return 1
[[ "$2" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || return 1
local -a a b
IFS=. read -ra a <<<"$1"
IFS=. read -ra b <<<"$2"
Expand Down Expand Up @@ -300,7 +302,7 @@ ensure_supported_runtime() {
[[ "$node_major" =~ ^[0-9]+$ ]] || error "Could not determine Node.js version from '${node_version}'. ${RUNTIME_REQUIREMENT_MSG}"
[[ "$npm_major" =~ ^[0-9]+$ ]] || error "Could not determine npm version from '${npm_version}'. ${RUNTIME_REQUIREMENT_MSG}"

if ((node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR)); then
if ! version_gte "${node_version#v}" "$MIN_NODE_VERSION" || ((npm_major < MIN_NPM_MAJOR)); then
error "Unsupported runtime detected: Node.js ${node_version:-unknown}, npm ${npm_version:-unknown}. ${RUNTIME_REQUIREMENT_MSG} Upgrade Node.js and rerun the installer."
fi

Expand Down Expand Up @@ -344,9 +346,9 @@ install_nodejs() {
spin "Installing nvm..." bash "$nvm_tmp"
rm -f "$nvm_tmp"
ensure_nvm_loaded
spin "Installing Node.js ${RECOMMENDED_NODE_MAJOR}..." bash -c ". \"$NVM_DIR/nvm.sh\" && nvm install ${RECOMMENDED_NODE_MAJOR} --no-progress"
spin "Installing Node.js 22..." bash -c ". \"$NVM_DIR/nvm.sh\" && nvm install 22 --no-progress"
ensure_nvm_loaded
nvm use "${RECOMMENDED_NODE_MAJOR}" --silent
nvm use 22 --silent
info "Node.js installed: $(node --version)"
}

Expand Down
14 changes: 12 additions & 2 deletions nemoclaw-blueprint/policies/presets/discord.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,25 @@ network_policies:
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- allow: { method: PUT, path: "/**" }
- allow: { method: PATCH, path: "/**" }
- allow: { method: DELETE, path: "/**" }
# WebSocket gateway — must use access: full (CONNECT tunnel) instead
# of protocol: rest. The proxy's HTTP idle timeout (~2 min) kills
# long-lived WebSocket connections; a CONNECT tunnel avoids
# HTTP-level timeouts entirely. See #409.
- host: gateway.discord.gg
port: 443
access: full
- host: cdn.discordapp.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: cdn.discordapp.com
# Media/attachment access (read-only, proxied through Discord CDN)
- host: media.discordapp.net
port: 443
protocol: rest
enforcement: enforce
Expand Down
10 changes: 9 additions & 1 deletion nemoclaw-blueprint/policies/presets/slack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

preset:
name: slack
description: "Slack API and webhooks access"
description: "Slack API, Socket Mode, and webhooks access"

network_policies:
slack:
Expand Down Expand Up @@ -33,5 +33,13 @@ network_policies:
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
# Socket Mode WebSocket — requires CONNECT tunnel to avoid
# HTTP idle timeout killing the persistent connection. See #409.
- host: wss-primary.slack.com
port: 443
access: full
- host: wss-backup.slack.com
port: 443
access: full
binaries:
- { path: /usr/local/bin/node }
Loading
Loading