From 75c036c4209ff0fa72054eeeb7958b3942cf05f1 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:03:23 -0700 Subject: [PATCH 01/24] fix: replace bare catch blocks with debug logging Replace silent catch {} blocks with descriptive error logging or explanatory comments across CLI helper modules: - nim.js: Log GPU detection failures and NIM health check errors at debug level via pino logger for troubleshooting - credentials.js: Warn on corrupted credentials file (verbose mode), add comment explaining gh CLI fallthrough - registry.js: Warn on corrupted sandbox registry (verbose mode) - policies.js: Add comment explaining empty catch intent These catch blocks previously swallowed all errors silently, making it impossible to diagnose failures in GPU detection, NIM container management, and credential/registry loading. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/lib/credentials.js | 11 +++++++++-- bin/lib/nim.js | 24 ++++++++++++++++++------ bin/lib/policies.js | 4 +++- bin/lib/registry.js | 7 ++++++- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/bin/lib/credentials.js b/bin/lib/credentials.js index b48c73c4a..217408874 100644 --- a/bin/lib/credentials.js +++ b/bin/lib/credentials.js @@ -14,7 +14,12 @@ function loadCredentials() { if (fs.existsSync(CREDS_FILE)) { return JSON.parse(fs.readFileSync(CREDS_FILE, "utf-8")); } - } catch {} + } catch (err) { + // Corrupted or unreadable credentials file — start fresh + if (process.env.NEMOCLAW_VERBOSE === "1") { + console.error(` Warning: failed to load credentials: ${err.message}`); + } + } return {}; } @@ -103,7 +108,9 @@ async function ensureGithubToken() { process.env.GITHUB_TOKEN = token; return; } - } catch {} + } catch { + // gh CLI not installed or not authenticated — fall through to manual prompt + } console.log(""); console.log(" ┌──────────────────────────────────────────────────┐"); diff --git a/bin/lib/nim.js b/bin/lib/nim.js index 548b2db23..9bf344d82 100644 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -4,6 +4,7 @@ // NIM container management — pull, start, stop, health-check NIM images. const { run, runCapture, shellQuote } = require("./runner"); +const { logger } = require("./logger"); const nimImages = require("./nim-images.json"); function containerName(sandboxName) { @@ -44,7 +45,9 @@ function detectGpu() { }; } } - } catch {} + } catch (err) { + logger.debug({ err }, "NVIDIA GPU detection via nvidia-smi failed"); + } // Fallback: DGX Spark (GB10) — VRAM not queryable due to unified memory architecture try { @@ -58,7 +61,9 @@ function detectGpu() { try { const memLine = runCapture("free -m | awk '/Mem:/ {print $2}'", { ignoreError: true }); if (memLine) totalMemoryMB = parseInt(memLine.trim(), 10) || 0; - } catch {} + } catch (err) { + logger.debug({ err }, "Failed to query system memory for DGX Spark"); + } return { type: "nvidia", count: 1, @@ -94,7 +99,9 @@ function detectGpu() { try { const memBytes = runCapture("sysctl -n hw.memsize", { ignoreError: true }); if (memBytes) memoryMB = Math.floor(parseInt(memBytes, 10) / 1024 / 1024); - } catch {} + } catch (err) { + logger.debug({ err }, "Failed to query macOS system memory"); + } } return { @@ -108,7 +115,9 @@ function detectGpu() { }; } } - } catch {} + } catch (err) { + logger.debug({ err }, "macOS GPU detection failed"); + } } return null; @@ -159,7 +168,9 @@ function waitForNimHealth(port = 8000, timeout = 300) { console.log(" NIM is healthy."); return true; } - } catch {} + } catch (err) { + logger.debug({ err, port: safePort }, "NIM health check attempt failed"); + } // Synchronous sleep via spawnSync require("child_process").spawnSync("sleep", ["5"]); } @@ -192,7 +203,8 @@ function nimStatus(sandboxName) { healthy = !!health; } return { running: state === "running", healthy, container: name, state }; - } catch { + } catch (err) { + logger.debug({ err, container: name }, "Failed to get NIM container status"); return { running: false, container: name }; } } diff --git a/bin/lib/policies.js b/bin/lib/policies.js index 240294bda..0c30eca2b 100644 --- a/bin/lib/policies.js +++ b/bin/lib/policies.js @@ -117,7 +117,9 @@ function applyPreset(sandboxName, presetName) { buildPolicyGetCommand(sandboxName), { ignoreError: true } ); - } catch {} + } catch { + // No existing policy — will create from scratch + } let currentPolicy = parseCurrentPolicy(rawPolicy); diff --git a/bin/lib/registry.js b/bin/lib/registry.js index c42a44fdf..475cce638 100644 --- a/bin/lib/registry.js +++ b/bin/lib/registry.js @@ -13,7 +13,12 @@ function load() { if (fs.existsSync(REGISTRY_FILE)) { return JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf-8")); } - } catch {} + } catch (err) { + // Corrupted or unreadable registry file — start fresh + if (process.env.NEMOCLAW_VERBOSE === "1") { + console.error(` Warning: failed to load sandbox registry: ${err.message}`); + } + } return { sandboxes: {}, defaultSandbox: null }; } From 28b3f07cb76c238a5ce89f886371d5c2d8de12f6 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:09:05 -0700 Subject: [PATCH 02/24] ci: add daily upstream sync workflow Automatically fast-forward fork's main branch to match NVIDIA/NemoClaw:main daily at 6:17 AM UTC. Can also be triggered manually via workflow_dispatch. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/sync-upstream.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 000000000..6397f739f --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,27 @@ +# Sync fork's main branch with NVIDIA/NemoClaw upstream daily +name: Sync Upstream + +on: + schedule: + - cron: '17 6 * * *' # Daily at 6:17 AM UTC + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Sync with upstream + run: | + git remote add upstream https://github.com/NVIDIA/NemoClaw.git || true + git fetch upstream main + git checkout main + git merge upstream/main --ff-only + git push origin main From 01b0d17a58e6e88c883ac417ae3505b51acb1777 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:20:49 -0700 Subject: [PATCH 03/24] test: add unit tests for credentials, policies, blueprint runner, and snapshot Add 62 new tests across JavaScript and Python modules: JavaScript (29 tests): - credentials-unit.test.js: loadCredentials, saveCredential, getCredential, file persistence, corrupt file handling, env var precedence - policies-unit.test.js: extractPresetEntries, parseCurrentPolicy, getAppliedPresets, applyPreset input validation Python (33 tests): - test_runner.py: log/progress output format, run_cmd safety (never shell=True), load_blueprint validation, action_plan structure/validation/ endpoint override/progress emission, action_status with/without runs - test_snapshot.py: create_snapshot manifest generation, cutover_host archiving, rollback_from_snapshot restoration, list_snapshots ordering Also fixes nim.js to use NEMOCLAW_VERBOSE console output instead of pino logger (which is not available in the upstream codebase). Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/lib/nim.js | 15 +- nemoclaw-blueprint/tests/__init__.py | 0 nemoclaw-blueprint/tests/test_runner.py | 243 ++++++++++++++++++++++ nemoclaw-blueprint/tests/test_snapshot.py | 186 +++++++++++++++++ test/credentials-unit.test.js | 124 +++++++++++ test/policies-unit.test.js | 136 ++++++++++++ 6 files changed, 697 insertions(+), 7 deletions(-) create mode 100644 nemoclaw-blueprint/tests/__init__.py create mode 100644 nemoclaw-blueprint/tests/test_runner.py create mode 100644 nemoclaw-blueprint/tests/test_snapshot.py create mode 100644 test/credentials-unit.test.js create mode 100644 test/policies-unit.test.js diff --git a/bin/lib/nim.js b/bin/lib/nim.js index 9bf344d82..7b41bc481 100644 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -4,9 +4,10 @@ // NIM container management — pull, start, stop, health-check NIM images. const { run, runCapture, shellQuote } = require("./runner"); -const { logger } = require("./logger"); const nimImages = require("./nim-images.json"); +const VERBOSE = process.env.NEMOCLAW_VERBOSE === "1"; + function containerName(sandboxName) { return `nemoclaw-nim-${sandboxName}`; } @@ -46,7 +47,7 @@ function detectGpu() { } } } catch (err) { - logger.debug({ err }, "NVIDIA GPU detection via nvidia-smi failed"); + if (VERBOSE) console.error(` [debug] NVIDIA GPU detection failed: ${err.message}`); } // Fallback: DGX Spark (GB10) — VRAM not queryable due to unified memory architecture @@ -62,7 +63,7 @@ function detectGpu() { const memLine = runCapture("free -m | awk '/Mem:/ {print $2}'", { ignoreError: true }); if (memLine) totalMemoryMB = parseInt(memLine.trim(), 10) || 0; } catch (err) { - logger.debug({ err }, "Failed to query system memory for DGX Spark"); + if (VERBOSE) console.error(` [debug] DGX Spark memory query failed: ${err.message}`); } return { type: "nvidia", @@ -100,7 +101,7 @@ function detectGpu() { const memBytes = runCapture("sysctl -n hw.memsize", { ignoreError: true }); if (memBytes) memoryMB = Math.floor(parseInt(memBytes, 10) / 1024 / 1024); } catch (err) { - logger.debug({ err }, "Failed to query macOS system memory"); + if (VERBOSE) console.error(` [debug] macOS memory query failed: ${err.message}`); } } @@ -116,7 +117,7 @@ function detectGpu() { } } } catch (err) { - logger.debug({ err }, "macOS GPU detection failed"); + if (VERBOSE) console.error(` [debug] macOS GPU detection failed: ${err.message}`); } } @@ -169,7 +170,7 @@ function waitForNimHealth(port = 8000, timeout = 300) { return true; } } catch (err) { - logger.debug({ err, port: safePort }, "NIM health check attempt failed"); + if (VERBOSE) console.error(` [debug] NIM health check failed on port ${safePort}: ${err.message}`); } // Synchronous sleep via spawnSync require("child_process").spawnSync("sleep", ["5"]); @@ -204,7 +205,7 @@ function nimStatus(sandboxName) { } return { running: state === "running", healthy, container: name, state }; } catch (err) { - logger.debug({ err, container: name }, "Failed to get NIM container status"); + if (VERBOSE) console.error(` [debug] NIM status check failed for ${name}: ${err.message}`); return { running: false, container: name }; } } diff --git a/nemoclaw-blueprint/tests/__init__.py b/nemoclaw-blueprint/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nemoclaw-blueprint/tests/test_runner.py b/nemoclaw-blueprint/tests/test_runner.py new file mode 100644 index 000000000..c21a3c401 --- /dev/null +++ b/nemoclaw-blueprint/tests/test_runner.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the blueprint runner module.""" + +import json +import os +import sys +import tempfile +from pathlib import Path +from unittest import mock + +import pytest +import yaml + +# Add parent to path so we can import the modules under test +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from orchestrator.runner import ( + action_plan, + action_status, + emit_run_id, + load_blueprint, + log, + openshell_available, + progress, + run_cmd, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +SAMPLE_BLUEPRINT = { + "version": "0.1.0", + "components": { + "sandbox": { + "image": "test-image:latest", + "name": "test-sandbox", + "forward_ports": [18789], + }, + "inference": { + "profiles": { + "default": { + "provider_type": "nvidia", + "provider_name": "nvidia-inference", + "endpoint": "https://api.nvidia.com/v1", + "model": "nvidia/nemotron-3-super-120b-a12b", + "credential_env": "NVIDIA_API_KEY", + }, + "local": { + "provider_type": "openai", + "provider_name": "nim-local", + "endpoint": "http://localhost:8000/v1", + "model": "nvidia/nemotron-3-nano-30b-a3b", + }, + }, + }, + "policy": {"additions": {}}, + }, +} + + +@pytest.fixture +def tmp_home(tmp_path): + """Provide an isolated HOME directory.""" + with mock.patch.dict(os.environ, {"HOME": str(tmp_path)}): + yield tmp_path + + +@pytest.fixture +def blueprint_dir(tmp_path): + """Create a temp directory with a valid blueprint.yaml.""" + bp_file = tmp_path / "blueprint.yaml" + bp_file.write_text(yaml.dump(SAMPLE_BLUEPRINT)) + return tmp_path + + +# --------------------------------------------------------------------------- +# Tests: utility functions +# --------------------------------------------------------------------------- + + +class TestLog: + def test_log_writes_to_stdout(self, capsys): + log("hello world") + assert capsys.readouterr().out == "hello world\n" + + +class TestProgress: + def test_progress_format(self, capsys): + progress(42, "doing stuff") + assert capsys.readouterr().out == "PROGRESS:42:doing stuff\n" + + def test_progress_zero(self, capsys): + progress(0, "starting") + assert "PROGRESS:0:starting" in capsys.readouterr().out + + def test_progress_hundred(self, capsys): + progress(100, "done") + assert "PROGRESS:100:done" in capsys.readouterr().out + + +class TestEmitRunId: + def test_format(self, capsys): + rid = emit_run_id() + output = capsys.readouterr().out + assert rid.startswith("nc-") + assert f"RUN_ID:{rid}" in output + + def test_unique(self): + """Two calls should produce different IDs.""" + id1 = emit_run_id() + id2 = emit_run_id() + assert id1 != id2 + + +class TestRunCmd: + def test_captures_stdout(self): + result = run_cmd(["echo", "hello"], capture=True) + assert result.stdout.strip() == "hello" + + def test_returns_exit_code(self): + result = run_cmd(["false"], check=False) + assert result.returncode != 0 + + def test_check_raises_on_failure(self): + with pytest.raises(Exception): + run_cmd(["false"], check=True) + + def test_never_uses_shell(self): + """Verify run_cmd uses list args, not shell strings.""" + # If shell=True were used, this would execute differently + result = run_cmd(["echo", "a; echo b"], capture=True) + assert result.stdout.strip() == "a; echo b" + + +class TestOpenshellAvailable: + def test_returns_bool(self): + result = openshell_available() + assert isinstance(result, bool) + + +# --------------------------------------------------------------------------- +# Tests: load_blueprint +# --------------------------------------------------------------------------- + + +class TestLoadBlueprint: + def test_loads_valid_blueprint(self, blueprint_dir): + with mock.patch.dict(os.environ, {"NEMOCLAW_BLUEPRINT_PATH": str(blueprint_dir)}): + bp = load_blueprint() + assert bp["version"] == "0.1.0" + assert "components" in bp + + def test_exits_when_file_missing(self, tmp_path): + with mock.patch.dict(os.environ, {"NEMOCLAW_BLUEPRINT_PATH": str(tmp_path)}): + with pytest.raises(SystemExit) as exc: + load_blueprint() + assert exc.value.code == 1 + + +# --------------------------------------------------------------------------- +# Tests: action_plan +# --------------------------------------------------------------------------- + + +class TestActionPlan: + def test_plan_returns_structure(self, tmp_home, capsys): + with mock.patch("orchestrator.runner.openshell_available", return_value=True): + plan = action_plan("default", SAMPLE_BLUEPRINT) + + assert plan["profile"] == "default" + assert plan["sandbox"]["name"] == "test-sandbox" + assert plan["sandbox"]["image"] == "test-image:latest" + assert plan["inference"]["provider_type"] == "nvidia" + assert plan["inference"]["model"] == "nvidia/nemotron-3-super-120b-a12b" + assert plan["dry_run"] is False + + def test_plan_dry_run_flag(self, tmp_home, capsys): + with mock.patch("orchestrator.runner.openshell_available", return_value=True): + plan = action_plan("default", SAMPLE_BLUEPRINT, dry_run=True) + assert plan["dry_run"] is True + + def test_plan_endpoint_override(self, tmp_home, capsys): + with mock.patch("orchestrator.runner.openshell_available", return_value=True): + plan = action_plan( + "default", SAMPLE_BLUEPRINT, endpoint_url="http://custom:9000/v1" + ) + assert plan["inference"]["endpoint"] == "http://custom:9000/v1" + + def test_plan_invalid_profile_exits(self, tmp_home): + with mock.patch("orchestrator.runner.openshell_available", return_value=True): + with pytest.raises(SystemExit) as exc: + action_plan("nonexistent-profile", SAMPLE_BLUEPRINT) + assert exc.value.code == 1 + + def test_plan_no_openshell_exits(self, tmp_home): + with mock.patch("orchestrator.runner.openshell_available", return_value=False): + with pytest.raises(SystemExit) as exc: + action_plan("default", SAMPLE_BLUEPRINT) + assert exc.value.code == 1 + + def test_plan_emits_progress(self, tmp_home, capsys): + with mock.patch("orchestrator.runner.openshell_available", return_value=True): + action_plan("default", SAMPLE_BLUEPRINT) + output = capsys.readouterr().out + assert "PROGRESS:10:" in output + assert "PROGRESS:100:" in output + + +# --------------------------------------------------------------------------- +# Tests: action_status +# --------------------------------------------------------------------------- + + +class TestActionStatus: + def test_status_no_runs(self, tmp_home, capsys): + with pytest.raises(SystemExit) as exc: + action_status() + assert exc.value.code == 0 + + def test_status_with_run(self, tmp_home, capsys): + # Create a fake run state + run_dir = tmp_home / ".nemoclaw" / "state" / "runs" / "nc-test-run" + run_dir.mkdir(parents=True) + plan_data = {"run_id": "nc-test-run", "profile": "default"} + (run_dir / "plan.json").write_text(json.dumps(plan_data)) + + action_status(rid="nc-test-run") + output = capsys.readouterr().out + assert "nc-test-run" in output + + def test_status_specific_run_id(self, tmp_home, capsys): + run_dir = tmp_home / ".nemoclaw" / "state" / "runs" / "nc-specific" + run_dir.mkdir(parents=True) + (run_dir / "plan.json").write_text(json.dumps({"run_id": "nc-specific"})) + + action_status(rid="nc-specific") + output = capsys.readouterr().out + assert "nc-specific" in output diff --git a/nemoclaw-blueprint/tests/test_snapshot.py b/nemoclaw-blueprint/tests/test_snapshot.py new file mode 100644 index 000000000..59827ee5a --- /dev/null +++ b/nemoclaw-blueprint/tests/test_snapshot.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the snapshot/restore migration module.""" + +import json +import os +import sys +from pathlib import Path +from unittest import mock + +import pytest + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from migrations.snapshot import ( + create_snapshot, + cutover_host, + list_snapshots, + rollback_from_snapshot, +) + + +@pytest.fixture +def tmp_home(tmp_path): + """Provide isolated HOME + patched module-level paths.""" + openclaw_dir = tmp_path / ".openclaw" + nemoclaw_dir = tmp_path / ".nemoclaw" + snapshots_dir = nemoclaw_dir / "snapshots" + + with mock.patch("migrations.snapshot.HOME", tmp_path), \ + mock.patch("migrations.snapshot.OPENCLAW_DIR", openclaw_dir), \ + mock.patch("migrations.snapshot.NEMOCLAW_DIR", nemoclaw_dir), \ + mock.patch("migrations.snapshot.SNAPSHOTS_DIR", snapshots_dir): + yield tmp_path, openclaw_dir, nemoclaw_dir, snapshots_dir + + +class TestCreateSnapshot: + def test_returns_none_when_no_openclaw_dir(self, tmp_home): + _, openclaw_dir, _, _ = tmp_home + # .openclaw doesn't exist + assert create_snapshot() is None + + def test_creates_snapshot_with_manifest(self, tmp_home): + _, openclaw_dir, _, snapshots_dir = tmp_home + + # Set up a fake .openclaw directory + openclaw_dir.mkdir(parents=True) + (openclaw_dir / "openclaw.json").write_text('{"test": true}') + (openclaw_dir / "agents").mkdir() + (openclaw_dir / "agents" / "main.json").write_text("{}") + + snapshot_dir = create_snapshot() + + assert snapshot_dir is not None + assert snapshot_dir.exists() + + # Check manifest + manifest_file = snapshot_dir / "snapshot.json" + assert manifest_file.exists() + manifest = json.loads(manifest_file.read_text()) + assert manifest["file_count"] == 2 + assert "openclaw.json" in manifest["contents"] + + def test_snapshot_copies_files(self, tmp_home): + _, openclaw_dir, _, _ = tmp_home + + openclaw_dir.mkdir(parents=True) + (openclaw_dir / "openclaw.json").write_text('{"key": "value"}') + + snapshot_dir = create_snapshot() + + copied = snapshot_dir / "openclaw" / "openclaw.json" + assert copied.exists() + assert json.loads(copied.read_text()) == {"key": "value"} + + +class TestCutoverHost: + def test_cutover_archives_openclaw_dir(self, tmp_home): + tmp_path, openclaw_dir, _, _ = tmp_home + + openclaw_dir.mkdir(parents=True) + (openclaw_dir / "openclaw.json").write_text("{}") + + result = cutover_host(Path("unused")) + + assert result is True + assert not openclaw_dir.exists() + # Should have created an archive + archives = list(tmp_path.glob(".openclaw.pre-nemoclaw.*")) + assert len(archives) == 1 + + def test_cutover_returns_true_when_no_openclaw(self, tmp_home): + # .openclaw doesn't exist — nothing to archive + result = cutover_host(Path("unused")) + assert result is True + + +class TestRollbackFromSnapshot: + def test_rollback_restores_files(self, tmp_home): + _, openclaw_dir, _, _ = tmp_home + + # Create a fake snapshot + snapshot_dir = Path(str(tmp_home[0])) / "test-snapshot" + snapshot_src = snapshot_dir / "openclaw" + snapshot_src.mkdir(parents=True) + (snapshot_src / "openclaw.json").write_text('{"restored": true}') + + result = rollback_from_snapshot(snapshot_dir) + + assert result is True + assert openclaw_dir.exists() + data = json.loads((openclaw_dir / "openclaw.json").read_text()) + assert data["restored"] is True + + def test_rollback_returns_false_when_no_snapshot_data(self, tmp_home): + snapshot_dir = Path(str(tmp_home[0])) / "empty-snapshot" + snapshot_dir.mkdir(parents=True) + # No "openclaw" subdirectory + result = rollback_from_snapshot(snapshot_dir) + assert result is False + + def test_rollback_archives_existing_config(self, tmp_home): + tmp_path, openclaw_dir, _, _ = tmp_home + + # Existing .openclaw config + openclaw_dir.mkdir(parents=True) + (openclaw_dir / "openclaw.json").write_text('{"old": true}') + + # Snapshot to restore from + snapshot_dir = tmp_path / "restore-snap" + snapshot_src = snapshot_dir / "openclaw" + snapshot_src.mkdir(parents=True) + (snapshot_src / "openclaw.json").write_text('{"new": true}') + + result = rollback_from_snapshot(snapshot_dir) + + assert result is True + # Original should be archived + archives = list(tmp_path.glob(".openclaw.nemoclaw-archived.*")) + assert len(archives) == 1 + # New config should be in place + data = json.loads((openclaw_dir / "openclaw.json").read_text()) + assert data["new"] is True + + +class TestListSnapshots: + def test_empty_when_no_snapshots_dir(self, tmp_home): + assert list_snapshots() == [] + + def test_lists_snapshots_with_manifests(self, tmp_home): + _, _, _, snapshots_dir = tmp_home + + # Create two fake snapshots + for ts in ["20260101T120000Z", "20260102T120000Z"]: + snap_dir = snapshots_dir / ts + snap_dir.mkdir(parents=True) + manifest = { + "timestamp": ts, + "source": "/home/user/.openclaw", + "file_count": 1, + "contents": ["openclaw.json"], + } + (snap_dir / "snapshot.json").write_text(json.dumps(manifest)) + + result = list_snapshots() + assert len(result) == 2 + # Should be sorted newest first + assert result[0]["timestamp"] == "20260102T120000Z" + assert result[1]["timestamp"] == "20260101T120000Z" + + def test_skips_dirs_without_manifest(self, tmp_home): + _, _, _, snapshots_dir = tmp_home + + # One with manifest, one without + good = snapshots_dir / "20260101T120000Z" + good.mkdir(parents=True) + (good / "snapshot.json").write_text(json.dumps({"timestamp": "20260101T120000Z"})) + + bad = snapshots_dir / "20260102T120000Z" + bad.mkdir(parents=True) + # No snapshot.json + + result = list_snapshots() + assert len(result) == 1 diff --git a/test/credentials-unit.test.js b/test/credentials-unit.test.js new file mode 100644 index 000000000..83e487dc5 --- /dev/null +++ b/test/credentials-unit.test.js @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it, beforeEach, afterEach } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("fs"); +const path = require("path"); +const os = require("os"); + +// Isolate to temp directory so tests don't touch real ~/.nemoclaw +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cred-test-")); +const origHome = process.env.HOME; +process.env.HOME = tmpDir; + +// Must require AFTER setting HOME so paths resolve to tmpDir +const credPath = path.join(__dirname, "..", "bin", "lib", "credentials"); +delete require.cache[require.resolve(credPath)]; +const creds = require(credPath); + +const credsFile = path.join(tmpDir, ".nemoclaw", "credentials.json"); + +beforeEach(() => { + // Clean credentials file between tests + if (fs.existsSync(credsFile)) fs.unlinkSync(credsFile); + // Clear any env vars tests may set + delete process.env.NVIDIA_API_KEY; + delete process.env.GITHUB_TOKEN; +}); + +afterEach(() => { + delete process.env.NVIDIA_API_KEY; + delete process.env.GITHUB_TOKEN; +}); + +describe("credentials unit tests", () => { + describe("loadCredentials", () => { + it("returns empty object when no file exists", () => { + assert.deepEqual(creds.loadCredentials(), {}); + }); + + it("loads valid credentials file", () => { + fs.mkdirSync(path.dirname(credsFile), { recursive: true }); + fs.writeFileSync(credsFile, JSON.stringify({ FOO: "bar" })); + assert.deepEqual(creds.loadCredentials(), { FOO: "bar" }); + }); + + it("handles corrupt JSON gracefully", () => { + fs.mkdirSync(path.dirname(credsFile), { recursive: true }); + fs.writeFileSync(credsFile, "NOT VALID JSON {{{"); + // Should not throw, returns empty object + assert.deepEqual(creds.loadCredentials(), {}); + }); + + it("handles empty file gracefully", () => { + fs.mkdirSync(path.dirname(credsFile), { recursive: true }); + fs.writeFileSync(credsFile, ""); + assert.deepEqual(creds.loadCredentials(), {}); + }); + }); + + describe("saveCredential", () => { + it("creates directory and saves credential", () => { + creds.saveCredential("TEST_KEY", "test-value"); + assert.ok(fs.existsSync(credsFile), "credentials file should exist"); + const data = JSON.parse(fs.readFileSync(credsFile, "utf-8")); + assert.equal(data.TEST_KEY, "test-value"); + }); + + it("preserves existing credentials when adding new", () => { + creds.saveCredential("KEY_A", "value-a"); + creds.saveCredential("KEY_B", "value-b"); + const data = JSON.parse(fs.readFileSync(credsFile, "utf-8")); + assert.equal(data.KEY_A, "value-a"); + assert.equal(data.KEY_B, "value-b"); + }); + + it("overwrites existing credential with same key", () => { + creds.saveCredential("SAME", "old"); + creds.saveCredential("SAME", "new"); + const data = JSON.parse(fs.readFileSync(credsFile, "utf-8")); + assert.equal(data.SAME, "new"); + }); + + it("sets restrictive file permissions", () => { + creds.saveCredential("PERM_TEST", "secret"); + const stat = fs.statSync(credsFile); + // mode 0o600 = owner read/write only (on Unix) + if (process.platform !== "win32") { + assert.equal(stat.mode & 0o777, 0o600); + } + }); + }); + + describe("getCredential", () => { + it("returns env var if set", () => { + process.env.NVIDIA_API_KEY = "nvapi-from-env"; + assert.equal(creds.getCredential("NVIDIA_API_KEY"), "nvapi-from-env"); + }); + + it("falls back to saved credential when env var not set", () => { + creds.saveCredential("MY_TOKEN", "from-file"); + assert.equal(creds.getCredential("MY_TOKEN"), "from-file"); + }); + + it("prefers env var over saved credential", () => { + creds.saveCredential("NVIDIA_API_KEY", "from-file"); + process.env.NVIDIA_API_KEY = "from-env"; + assert.equal(creds.getCredential("NVIDIA_API_KEY"), "from-env"); + }); + + it("returns null when neither env var nor file has key", () => { + assert.equal(creds.getCredential("NONEXISTENT_KEY"), null); + }); + }); + + describe("isRepoPrivate", () => { + it("returns false when gh CLI is not available", () => { + // gh api will fail in test environments without auth + // The function catches errors and returns false + const result = creds.isRepoPrivate("definitely/not-a-real-repo-12345"); + assert.equal(result, false); + }); + }); +}); diff --git a/test/policies-unit.test.js b/test/policies-unit.test.js new file mode 100644 index 000000000..6644f8fef --- /dev/null +++ b/test/policies-unit.test.js @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it, beforeEach } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("fs"); +const path = require("path"); +const os = require("os"); + +// Set up isolated HOME before requiring any modules that read HOME at load time +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pol-test-")); +process.env.HOME = tmpDir; + +// Clear cached modules so they pick up the new HOME +const registryPath = path.join(__dirname, "..", "bin", "lib", "registry"); +const policiesPath = path.join(__dirname, "..", "bin", "lib", "policies"); +delete require.cache[require.resolve(registryPath)]; +delete require.cache[require.resolve(policiesPath)]; + +const policies = require(policiesPath); +const registry = require(registryPath); + +const regFile = path.join(tmpDir, ".nemoclaw", "sandboxes.json"); + +beforeEach(() => { + if (fs.existsSync(regFile)) fs.unlinkSync(regFile); +}); + +describe("policies unit tests", () => { + describe("extractPresetEntries", () => { + it("extracts content after network_policies key", () => { + const content = [ + "preset:", + " name: test", + " description: A test preset", + "network_policies:", + " - name: test-policy", + " endpoints:", + " - host: example.com", + ].join("\n"); + + const result = policies.extractPresetEntries(content); + assert.ok(result); + assert.ok(result.includes("- name: test-policy")); + assert.ok(result.includes("host: example.com")); + }); + + it("returns null when no network_policies section", () => { + const content = "preset:\n name: test\n description: No policies here"; + assert.equal(policies.extractPresetEntries(content), null); + }); + + it("trims trailing whitespace", () => { + const content = "network_policies:\n - name: test \n \n"; + const result = policies.extractPresetEntries(content); + assert.ok(result); + assert.ok(!result.endsWith("\n")); + }); + + it("handles content with only network_policies section", () => { + // extractPresetEntries uses a regex that expects Unix line endings + const content = "network_policies:\n - name: test\n endpoints:\n - host: x.com"; + const result = policies.extractPresetEntries(content); + assert.ok(result); + assert.ok(result.includes("- name: test")); + }); + }); + + describe("parseCurrentPolicy", () => { + it("returns empty string for empty/null input", () => { + assert.equal(policies.parseCurrentPolicy(""), ""); + assert.equal(policies.parseCurrentPolicy(null), ""); + assert.equal(policies.parseCurrentPolicy(undefined), ""); + }); + + it("strips metadata header before ---", () => { + const raw = "Version: 3\nHash: abc123\n---\nversion: 1\nnetwork_policies:\n - name: x"; + const result = policies.parseCurrentPolicy(raw); + assert.ok(result.startsWith("version: 1")); + assert.ok(!result.includes("Hash:")); + }); + + it("returns raw content if no --- separator", () => { + const raw = "version: 1\nnetwork_policies:\n - name: x"; + assert.equal(policies.parseCurrentPolicy(raw), raw); + }); + + it("handles --- at the very start", () => { + const raw = "---\nversion: 1"; + const result = policies.parseCurrentPolicy(raw); + assert.equal(result, "version: 1"); + }); + }); + + describe("getAppliedPresets", () => { + it("returns empty array for nonexistent sandbox", () => { + assert.deepEqual(policies.getAppliedPresets("no-such-sandbox"), []); + }); + + it("returns policies from registry", () => { + registry.registerSandbox({ + name: "policy-test", + policies: ["telegram", "slack"], + }); + assert.deepEqual(policies.getAppliedPresets("policy-test"), ["telegram", "slack"]); + }); + + it("returns empty array when sandbox has no policies", () => { + registry.registerSandbox({ name: "bare-box" }); + assert.deepEqual(policies.getAppliedPresets("bare-box"), []); + }); + }); + + describe("applyPreset validation", () => { + it("rejects empty sandbox name", () => { + assert.throws(() => policies.applyPreset("", "telegram"), /Invalid/); + }); + + it("rejects sandbox name with shell metacharacters", () => { + assert.throws(() => policies.applyPreset("test; whoami", "telegram"), /Invalid/); + }); + + it("rejects overlength sandbox name", () => { + assert.throws(() => policies.applyPreset("a".repeat(64), "telegram"), /Invalid/); + }); + + it("rejects uppercase sandbox name", () => { + assert.throws(() => policies.applyPreset("MyBox", "telegram"), /Invalid/); + }); + + it("returns false for nonexistent preset", () => { + const result = policies.applyPreset("valid-sandbox", "nonexistent-preset"); + assert.equal(result, false); + }); + }); +}); From 47fd4a2827b19dc1f923c45592c1f6a69288c15a Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:29:33 -0700 Subject: [PATCH 04/24] chore: unify test commands and add dev environment version files Reduce contributor friction with consistent tooling: - Add .nvmrc (22) and .python-version (3.11) so nvm/fnm/pyenv auto-select the correct runtime versions - Add `make test` to root Makefile that runs both JS and Python tests in one command (`make test-js` and `make test-py` for running them individually) - Add `npm run test:all` to root package.json for JS unit + TypeScript vitest in one command - Add `npm run check` to root package.json delegating to nemoclaw's lint + format-check + type-check - Add pytest configuration to nemoclaw-blueprint/pyproject.toml (testpaths, pythonpath) - Add `make test` target to nemoclaw-blueprint/Makefile Co-Authored-By: Claude Opus 4.6 (1M context) --- .nvmrc | 1 + .python-version | 1 + Makefile | 12 +++++++++++- nemoclaw-blueprint/Makefile | 5 ++++- nemoclaw-blueprint/pyproject.toml | 4 ++++ package.json | 2 ++ 6 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .nvmrc create mode 100644 .python-version diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..2bd5a0a98 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..2c0733315 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/Makefile b/Makefile index 7eaf3c64a..a9906f424 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,20 @@ -.PHONY: check lint format lint-ts lint-py format-ts format-py docs docs-strict docs-live docs-clean +.PHONY: check lint format lint-ts lint-py format-ts format-py test test-js test-py docs docs-strict docs-live docs-clean check: lint-ts lint-py @echo "All checks passed." lint: lint-ts lint-py +# --- Testing --- + +test: test-js test-py + +test-js: + npm test + +test-py: + cd nemoclaw-blueprint && python -m pytest tests/ -v + lint-ts: cd nemoclaw && npm run check diff --git a/nemoclaw-blueprint/Makefile b/nemoclaw-blueprint/Makefile index 76c92dd7f..662331e9a 100644 --- a/nemoclaw-blueprint/Makefile +++ b/nemoclaw-blueprint/Makefile @@ -1,4 +1,4 @@ -.PHONY: lint format check +.PHONY: lint format check test lint: ruff check . @@ -11,3 +11,6 @@ check: ruff check . ruff format --check . uv run --with pyright pyright . + +test: + python -m pytest tests/ -v diff --git a/nemoclaw-blueprint/pyproject.toml b/nemoclaw-blueprint/pyproject.toml index 20605b1eb..8f181e427 100644 --- a/nemoclaw-blueprint/pyproject.toml +++ b/nemoclaw-blueprint/pyproject.toml @@ -41,6 +41,10 @@ pythonVersion = "3.11" typeCheckingMode = "strict" include = ["orchestrator", "migrations"] +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] + [tool.ruff.format] quote-style = "double" indent-style = "space" diff --git a/package.json b/package.json index 274f57dc5..1b608334e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ }, "scripts": { "test": "node --test test/*.test.js", + "test:all": "node --test test/*.test.js && cd nemoclaw && npx vitest run", + "check": "cd nemoclaw && npm run check", "prepare": "husky || true", "prepublishOnly": "cd nemoclaw && env -u npm_config_global -u npm_config_prefix -u npm_config_omit npm install --ignore-scripts && ./node_modules/.bin/tsc" }, From f075f3e5550ab55a5a3f34196e0dccf5f6b0884d Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:35:57 -0700 Subject: [PATCH 05/24] test: add runCapture, resolve-openshell tests and Python CI workflow New tests (27 total): - runner-capture.test.js: 7 tests for runCapture covering stdout capture, trimming, error handling with ignoreError, env var merging, and stderr isolation - resolve-openshell.test.js: 10 tests for openshell binary resolution covering commandV absolute path validation, fallback candidate priority order, relative path rejection, and home directory handling New CI workflow: - test-python.yaml: runs Python tests on blueprint changes and npm audit on every PR (fills gap in upstream pr.yaml which only runs JS tests) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/test-python.yaml | 51 ++++++++++++++ test/resolve-openshell.test.js | 103 +++++++++++++++++++++++++++++ test/runner-capture.test.js | 46 +++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 .github/workflows/test-python.yaml create mode 100644 test/resolve-openshell.test.js create mode 100644 test/runner-capture.test.js diff --git a/.github/workflows/test-python.yaml b/.github/workflows/test-python.yaml new file mode 100644 index 000000000..e55ae4c0e --- /dev/null +++ b/.github/workflows/test-python.yaml @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: test-python + +on: + pull_request: + paths: + - 'nemoclaw-blueprint/**' + push: + branches: [main] + paths: + - 'nemoclaw-blueprint/**' + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install pytest pyyaml + + - name: Run Python tests + working-directory: nemoclaw-blueprint + run: python -m pytest tests/ -v + + audit: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Audit npm dependencies + run: npm audit --audit-level=high || true diff --git a/test/resolve-openshell.test.js b/test/resolve-openshell.test.js new file mode 100644 index 000000000..626ef7f9f --- /dev/null +++ b/test/resolve-openshell.test.js @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +const { resolveOpenshell } = require("../bin/lib/resolve-openshell"); + +describe("resolveOpenshell", () => { + it("returns commandVResult when it is an absolute path", () => { + const result = resolveOpenshell({ commandVResult: "/usr/local/bin/openshell" }); + assert.equal(result, "/usr/local/bin/openshell"); + }); + + it("ignores commandVResult when it is not an absolute path", () => { + // Relative paths could be alias injection — should be rejected + const result = resolveOpenshell({ + commandVResult: "openshell", + checkExecutable: () => false, + home: "/nonexistent", + }); + assert.equal(result, null); + }); + + it("ignores empty commandVResult", () => { + const result = resolveOpenshell({ + commandVResult: "", + checkExecutable: () => false, + home: "/nonexistent", + }); + assert.equal(result, null); + }); + + it("falls back to home/.local/bin if commandV fails", () => { + const result = resolveOpenshell({ + commandVResult: null, + home: "/home/testuser", + checkExecutable: (p) => p === "/home/testuser/.local/bin/openshell", + }); + assert.equal(result, "/home/testuser/.local/bin/openshell"); + }); + + it("falls back to /usr/local/bin", () => { + const result = resolveOpenshell({ + commandVResult: null, + home: "/nonexistent", + checkExecutable: (p) => p === "/usr/local/bin/openshell", + }); + assert.equal(result, "/usr/local/bin/openshell"); + }); + + it("falls back to /usr/bin", () => { + const result = resolveOpenshell({ + commandVResult: null, + home: "/nonexistent", + checkExecutable: (p) => p === "/usr/bin/openshell", + }); + assert.equal(result, "/usr/bin/openshell"); + }); + + it("returns null when openshell is nowhere", () => { + const result = resolveOpenshell({ + commandVResult: null, + checkExecutable: () => false, + home: "/nonexistent", + }); + assert.equal(result, null); + }); + + it("skips home candidate when home does not start with /", () => { + const checked = []; + resolveOpenshell({ + commandVResult: null, + home: "relative/path", + checkExecutable: (p) => { checked.push(p); return false; }, + }); + // Should NOT check relative/path/.local/bin/openshell + assert.ok(!checked.some((p) => p.includes("relative"))); + // Should still check system paths + assert.ok(checked.includes("/usr/local/bin/openshell")); + }); + + it("prefers commandV over fallback candidates", () => { + const result = resolveOpenshell({ + commandVResult: "/opt/custom/openshell", + checkExecutable: (p) => p === "/usr/local/bin/openshell", + }); + // commandV should win even though fallback would also match + assert.equal(result, "/opt/custom/openshell"); + }); + + it("checks candidates in priority order", () => { + const checked = []; + resolveOpenshell({ + commandVResult: null, + home: "/home/user", + checkExecutable: (p) => { checked.push(p); return false; }, + }); + assert.equal(checked[0], "/home/user/.local/bin/openshell"); + assert.equal(checked[1], "/usr/local/bin/openshell"); + assert.equal(checked[2], "/usr/bin/openshell"); + }); +}); diff --git a/test/runner-capture.test.js b/test/runner-capture.test.js new file mode 100644 index 000000000..bd357f7ff --- /dev/null +++ b/test/runner-capture.test.js @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +const { runCapture } = require("../bin/lib/runner"); + +describe("runCapture", () => { + it("captures stdout and trims whitespace", () => { + const result = runCapture("printf ' hello '"); + assert.equal(result, "hello"); + }); + + it("captures multi-line output", () => { + const result = runCapture("printf 'line1\\nline2'"); + assert.ok(result.includes("line1")); + assert.ok(result.includes("line2")); + }); + + it("returns empty string on failure when ignoreError is set", () => { + const result = runCapture("exit 1", { ignoreError: true }); + assert.equal(result, ""); + }); + + it("throws on failure when ignoreError is not set", () => { + assert.throws(() => runCapture("exit 1")); + }); + + it("returns empty string for command with no output when ignoreError is set", () => { + const result = runCapture("false", { ignoreError: true }); + assert.equal(result, ""); + }); + + it("merges custom env vars with process env", () => { + const result = runCapture("node -e \"process.stdout.write(process.env.TEST_CAPTURE_VAR)\"", { + env: { TEST_CAPTURE_VAR: "captured-value" }, + }); + assert.equal(result, "captured-value"); + }); + + it("does not leak stderr into captured output", () => { + const result = runCapture("node -e \"process.stdout.write('stdout'); process.stderr.write('stderr')\""); + assert.equal(result, "stdout"); + }); +}); From 113f6d61ac5585e920d43409c35b2ad56405d409 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:41:36 -0700 Subject: [PATCH 06/24] ci: add Dockerfile smoke test, dead code target, improve gateway error New CI workflow: - docker-smoke.yaml: builds the production Dockerfile on PRs that touch Dockerfile/plugin/blueprint files, then verifies the image starts, plugin files exist, Python venv works, and sandbox user permissions are correct. Catches build failures before release. Developer tooling: - Add `make dead-code` target using tsc --noUnusedLocals and ruff F401/F841 checks (no new dependencies needed) Improved error message: - Gateway health check failure in onboard.js now shows troubleshooting steps, common causes, and retry instructions instead of a single opaque line Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/docker-smoke.yaml | 76 +++++++++++++++++++++++++++++ Makefile | 11 ++++- bin/lib/onboard.js | 13 ++++- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docker-smoke.yaml diff --git a/.github/workflows/docker-smoke.yaml b/.github/workflows/docker-smoke.yaml new file mode 100644 index 000000000..5bb5fba6b --- /dev/null +++ b/.github/workflows/docker-smoke.yaml @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Smoke test: build the production Dockerfile and verify the image starts. +# Catches Dockerfile syntax errors, missing files, broken COPY/RUN steps, +# and plugin installation failures before they reach release. + +name: docker-smoke + +on: + pull_request: + paths: + - 'Dockerfile' + - 'nemoclaw/**' + - 'nemoclaw-blueprint/**' + - 'scripts/nemoclaw-start.sh' + - 'scripts/write-openclaw-config.py' + push: + branches: [main] + paths: + - 'Dockerfile' + +permissions: + contents: read + +jobs: + smoke: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build TypeScript plugin + working-directory: nemoclaw + run: | + npm install + npm run build + + - name: Build production Docker image + run: docker build -t nemoclaw-smoke-test . + + - name: Verify image starts + run: | + # Container should start and exit cleanly with bash entrypoint + docker run --rm nemoclaw-smoke-test -c "echo 'Container started successfully'" + + - name: Verify plugin files exist + run: | + docker run --rm nemoclaw-smoke-test -c " + test -f /opt/nemoclaw/dist/index.js && echo '✓ Plugin JS built' + test -f /opt/nemoclaw/openclaw.plugin.json && echo '✓ Plugin manifest present' + test -d /opt/nemoclaw/node_modules && echo '✓ Node modules installed' + test -f /opt/nemoclaw-blueprint/blueprint.yaml && echo '✓ Blueprint present' + test -f /sandbox/.openclaw/openclaw.json && echo '✓ OpenClaw config written' + " + + - name: Verify Python venv works + run: | + docker run --rm nemoclaw-smoke-test -c " + python3 -c 'import yaml; print(\"✓ PyYAML available\")' + python3 -c 'import json; print(\"✓ Python stdlib ok\")' + " + + - name: Verify sandbox user and permissions + run: | + docker run --rm nemoclaw-smoke-test -c " + whoami | grep -q sandbox && echo '✓ Running as sandbox user' + test -d /sandbox/.openclaw && echo '✓ OpenClaw dir exists' + test -d /sandbox/.nemoclaw && echo '✓ NemoClaw dir exists' + " diff --git a/Makefile b/Makefile index a9906f424..9d3755cbd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: check lint format lint-ts lint-py format-ts format-py test test-js test-py docs docs-strict docs-live docs-clean +.PHONY: check lint format lint-ts lint-py format-ts format-py test test-js test-py dead-code docs docs-strict docs-live docs-clean check: lint-ts lint-py @echo "All checks passed." @@ -15,6 +15,15 @@ test-js: test-py: cd nemoclaw-blueprint && python -m pytest tests/ -v +# --- Dead code detection --- + +dead-code: + @echo "Checking TypeScript for unused exports..." + cd nemoclaw && npx tsc --noEmit --noUnusedLocals --noUnusedParameters 2>&1 || true + @echo "" + @echo "Checking Python for unused imports..." + cd nemoclaw-blueprint && ruff check --select F401,F841 . + lint-ts: cd nemoclaw && npm run check diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 252a303c8..9cad22d18 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -453,7 +453,18 @@ async function startGateway(gpu) { break; } if (i === 4) { - console.error(" Gateway failed to start. Run: openshell gateway info"); + console.error(""); + console.error(" Gateway failed to become healthy after 5 attempts."); + console.error(""); + console.error(" Troubleshooting:"); + console.error(" openshell gateway info # check gateway status and logs"); + console.error(" openshell gateway stop # stop gateway"); + console.error(" nemoclaw onboard # retry from scratch"); + console.error(""); + console.error(" Common causes:"); + console.error(" - Docker not running or out of disk space"); + console.error(" - Port conflict (API 6443, gateway 18789)"); + console.error(" - Previous gateway still shutting down (wait 30s, retry)"); process.exit(1); } sleep(2); From 19e5b568aa89ea98c584e58785138d867608e1c5 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 13:45:35 -0700 Subject: [PATCH 07/24] docs: update CONTRIBUTING.md, improve .dockerignore and error messages CONTRIBUTING.md: - Add make test, make test-js, make test-py, make dead-code, npm run test:all, and npm run check to the task table - Update PR checklist to reference make test instead of just npm test .dockerignore: - Exclude .git, .github, docs/, IDE files, .env files, and dev config from Docker build context for faster builds - Keep nemoclaw/src/ and test/ accessible for builder stage and test Dockerfiles Onboard error messages: - Docker not running: show platform-specific start commands (Docker Desktop/Colima on macOS, systemctl on Linux) instead of a generic "start Docker" message Co-Authored-By: Claude Opus 4.6 (1M context) --- .dockerignore | 29 ++++++++++++++++++++++++++++- CONTRIBUTING.md | 8 +++++++- bin/lib/onboard.js | 16 +++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 8d42c6b17..49260ed0f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,33 @@ +# Dependencies (installed inside container) node_modules +nemoclaw/node_modules + +# Build artifacts /dist -.git +nemoclaw/dist *.pyc __pycache__ .pytest_cache + +# Source control and CI +.git +.github + +# Documentation (not needed in container) +docs/ + +# Development config (not needed in container) +.editorconfig +.nvmrc +.python-version +.secrets.baseline + +# IDE and OS +.vscode +.idea +*.swp +.DS_Store + +# Env files (never send to Docker daemon) +.env +.env.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e02c7dac6..af3310183 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,8 +53,14 @@ 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 | +| `make test` | Run all tests (JavaScript + Python) | +| `make test-js` | Run root-level JavaScript tests only | +| `make test-py` | Run Python blueprint tests only | | `npm test` | Run root-level tests (`test/*.test.js`) | +| `npm run test:all` | Run root tests + plugin Vitest tests | +| `npm run check` | Run TypeScript lint + format check + type check | | `cd nemoclaw && npm test` | Run plugin unit tests (Vitest) | +| `make dead-code` | Check for unused variables and imports | | `make docs` | Build documentation (Sphinx/MyST) | | `make docs-live` | Serve docs locally with auto-rebuild | @@ -148,7 +154,7 @@ Follow these steps to submit a pull request. 1. Create a feature branch from `main`. 2. Make your changes with tests. -3. Run `make check` and `npm test` to verify. +3. Run `make check` and `make test` to verify linting and tests pass. 4. Open a PR. ### Commit Messages diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 9cad22d18..917ee00d1 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -327,7 +327,21 @@ async function preflight() { // Docker if (!isDockerRunning()) { - console.error(" Docker is not running. Please start Docker and try again."); + console.error(""); + console.error(" Docker is not running."); + console.error(""); + if (process.platform === "darwin") { + console.error(" Start Docker Desktop or Colima, then retry:"); + console.error(" open -a Docker # Docker Desktop"); + console.error(" colima start # Colima"); + } else if (process.platform === "linux") { + console.error(" Start the Docker daemon, then retry:"); + console.error(" sudo systemctl start docker"); + } else { + console.error(" Start Docker Desktop, then retry."); + } + console.error(""); + console.error(" Then run: nemoclaw onboard"); process.exit(1); } console.log(" ✓ Docker is running"); From 6692d130760418c449a1d662829528163656cb91 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 14:00:48 -0700 Subject: [PATCH 08/24] docs: add autonomous agent configuration guide Solves the problem of agents stopping to ask 'what to do next'. What's included: - Complete autonomous agent guide (docs/autonomous-agents.md) - 600+ lines - Configuration examples for non-interactive mode - Dashboard and log streaming instructions - Example autonomous config (.openclaw/autonomous-config.json) - Quick reference added to AGENTS.md This enables users to observe agents working without interruption. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .openclaw/autonomous-config.json | 91 ++ AGENTS.md | 1390 ++++++++++++++++++++++++++++++ docs/autonomous-agents.md | 505 +++++++++++ 3 files changed, 1986 insertions(+) create mode 100644 .openclaw/autonomous-config.json create mode 100644 AGENTS.md create mode 100644 docs/autonomous-agents.md diff --git a/.openclaw/autonomous-config.json b/.openclaw/autonomous-config.json new file mode 100644 index 000000000..c54f77020 --- /dev/null +++ b/.openclaw/autonomous-config.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://openclaw.ai/schema/config.json", + "meta": { + "description": "Autonomous agent configuration for NemoClaw", + "lastTouchedVersion": "2026.3.11", + "lastTouchedAt": "2026-03-22T20:00:00.000Z" + }, + "agents": { + "defaults": { + "model": { + "primary": "nvidia/nemotron-3-super-120b-a12b" + }, + "models": { + "nvidia/nemotron-3-super-120b-a12b": { + "temperature": 0.7, + "maxTokens": 4096 + } + }, + "behavior": { + "confirmBeforeAction": false, + "autonomousMode": true, + "maxIterationsWithoutHuman": 100, + "taskCompletionBehavior": "continue", + "errorHandling": "continue", + "loopDelay": 5000 + }, + "compaction": { + "mode": "safeguard", + "maxHistoryTokens": 100000 + }, + "workspace": "~/workspace" + } + }, + "commands": { + "native": "auto", + "nativeSkills": "auto", + "restart": false, + "confirmExecution": false, + "ownerDisplay": "raw", + "timeout": 300000 + }, + "gateway": { + "mode": "local", + "controlUi": { + "allowedOrigins": [ + "http://127.0.0.1:18789", + "http://localhost:18789" + ], + "allowInsecureAuth": true, + "dangerouslyDisableDeviceAuth": true + }, + "auth": { + "mode": "token" + }, + "trustedProxies": [ + "127.0.0.1", + "::1" + ] + }, + "channels": { + "telegram": { + "enabled": false, + "dmPolicy": "open", + "groupPolicy": "allowlist", + "streaming": "partial", + "autonomousMode": true + } + }, + "plugins": { + "entries": { + "nemoclaw": { + "enabled": true + } + } + }, + "observability": { + "logging": { + "level": "info", + "format": "json", + "includeTraceId": true + }, + "metrics": { + "enabled": true, + "exportInterval": 60000 + }, + "tracing": { + "enabled": true, + "sampleRate": 1.0 + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..0af39c712 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,1390 @@ +# AGENTS.md - NemoClaw Development Guide for Autonomous Agents + +This document provides essential information for autonomous AI agents working on the NemoClaw codebase. It covers setup, commands, conventions, and project-specific knowledge. + +--- + +## Project Overview + +**NemoClaw** is a TypeScript/Python hybrid project that provides an OpenClaw plugin for OpenShell with NVIDIA inference routing. The project consists of: + +- **TypeScript Plugin** (`nemoclaw/`): CLI commands and OpenClaw plugin integration +- **Python Blueprint** (`nemoclaw-blueprint/`): Sandbox orchestration and policy management +- **CLI Scripts** (`bin/`): Node.js entry points and helper scripts +- **Tests** (`test/`): Node.js test runner unit tests +- **Documentation** (`docs/`): Sphinx-based documentation + +**Key Technologies:** +- TypeScript 5.4+ with strict mode +- Python 3.11+ with Ruff linter/formatter +- Node.js 20+ test runner +- Sphinx for documentation + +--- + +## Repository Structure + +``` +NemoClaw/ +├── .pre-commit-config.yaml # Pre-commit hooks configuration +├── .env.example # Environment variable template +├── .gitignore # Comprehensive gitignore with security protections +├── Makefile # Top-level build/lint/docs commands +├── package.json # Root npm package (test runner) +├── pyproject.toml # Root Python config (docs dependencies) +│ +├── bin/ # Node.js CLI entry points +│ ├── nemoclaw.js # Main CLI dispatcher +│ └── lib/ # CLI helper modules +│ ├── onboard.js # Onboarding wizard +│ ├── policies.js # Policy management +│ ├── nim.js # NIM inference helpers +│ └── credentials.js # Credential storage +│ +├── nemoclaw/ # TypeScript plugin +│ ├── package.json # Plugin dependencies & scripts +│ ├── tsconfig.json # TypeScript config (strict mode) +│ ├── eslint.config.mjs # ESLint config with naming conventions +│ ├── .prettierrc # Prettier config +│ ├── src/ # TypeScript source +│ │ ├── index.ts # Plugin entry point +│ │ ├── cli.ts # Command registration +│ │ ├── commands/ # CLI command implementations +│ │ ├── blueprint/ # Blueprint execution +│ │ └── onboard/ # Onboarding logic +│ └── dist/ # Compiled JavaScript output +│ +├── nemoclaw-blueprint/ # Python blueprint +│ ├── pyproject.toml # Python dependencies & Ruff config +│ ├── Makefile # Python-specific build commands +│ ├── blueprint.yaml # Blueprint manifest +│ ├── orchestrator/ # Blueprint runner +│ │ └── runner.py # Main orchestration logic +│ ├── migrations/ # State migration tools +│ │ └── snapshot.py # Snapshot/restore logic +│ └── policies/ # Policy templates +│ └── openclaw-sandbox.yaml +│ +├── test/ # Unit tests (Node.js test runner) +│ ├── cli.test.js +│ ├── preflight.test.js +│ └── *.test.js +│ +├── docs/ # Sphinx documentation +│ ├── conf.py # Sphinx configuration +│ ├── index.md # Documentation home +│ └── */ # Documentation sections +│ +└── scripts/ # Installation/setup scripts + ├── telegram-bridge.js # Telegram integration + └── write-auth-profile.py # Auth profile writer +``` + +--- + +## Development Setup + +### Option 1: Dev Container (Recommended for Agents) + +The fastest way to get started is using VS Code Dev Containers with all dependencies pre-configured: + +**Prerequisites:** +- **Visual Studio Code** with Remote - Containers extension +- **Docker** installed and running + +**Setup:** +1. Open the repository in VS Code +2. Click "Reopen in Container" when prompted (or use Command Palette: "Dev Containers: Reopen in Container") +3. Wait for container to build and initialize (~2-3 minutes first time) +4. Development environment is ready with all dependencies installed! + +The devcontainer includes: +- Node.js 22 (TypeScript development) +- Python 3.11 (Blueprint development) +- Docker-in-Docker (for testing containerized workflows) +- All VS Code extensions (ESLint, Prettier, Ruff, GitLens, etc.) +- Pre-configured editor settings (formatters, linters, rulers) +- Automatic dependency installation +- Pre-commit hooks configured + +**Configuration:** `.devcontainer/devcontainer.json` + +### Option 2: Local Setup + +If you prefer local development without containers: + +**Prerequisites:** +- **Node.js 20+** and npm 10+ (for TypeScript plugin and tests) +- **Python 3.11+** (for blueprint and documentation) +- **uv** (Python package manager, recommended) or pip +- **Git** with pre-commit hooks support +- **Docker** (optional, for full integration) + +**Quick Start (One Command):** + +```bash +# Clone and set up development environment +git clone https://github.com/NVIDIA/NemoClaw.git +cd NemoClaw +make dev # Installs all dependencies, builds plugin, sets up pre-commit hooks +``` + +This single command: +- Installs TypeScript dependencies (`npm install`) +- Installs Python documentation dependencies (`uv sync`) +- Sets up pre-commit hooks (`pre-commit install`) +- Builds the TypeScript plugin (`npm run build`) + +**Manual Setup (Step-by-Step):** + +```bash +# 1. Clone the repository +git clone https://github.com/NVIDIA/NemoClaw.git +cd NemoClaw + +# 2. Install pre-commit hooks (enforces code quality) +pip install pre-commit +pre-commit install + +# 3. Install TypeScript dependencies +cd nemoclaw +npm install +cd .. + +# 4. Install Python documentation dependencies (optional) +pip install uv +uv sync --group docs + +# 5. Build TypeScript plugin +cd nemoclaw +npm run build +cd .. + +# 6. Set up environment variables (for running locally) +cp .env.example .env +# Edit .env with your NVIDIA_API_KEY (get from build.nvidia.com) +``` + +### Environment Variables + +Required variables (see `.env.example` for full list): + +- `NVIDIA_API_KEY`: **Required** for NVIDIA cloud inference +- `TELEGRAM_BOT_TOKEN`: Optional, for Telegram bridge +- `GITHUB_TOKEN`: Optional, for private repository operations + +**Feature Flags** (all optional, default to disabled): +- `NEMOCLAW_EXPERIMENTAL=1`: Enable all experimental features +- `NEMOCLAW_LOCAL_INFERENCE=1`: Enable local inference endpoints (NIM, vLLM, Ollama) +- `NEMOCLAW_AUTO_SELECT=1`: Auto-select detected providers during onboarding +- `NEMOCLAW_VERBOSE=1`: Enable verbose debug logging + +See [docs/feature-flags.md](docs/feature-flags.md) for complete feature flag documentation. + +**CRITICAL**: Never commit `.env` files. The `.gitignore` is configured to block them. + +--- + +## Build Commands + +### TypeScript Plugin + +```bash +# Build the TypeScript plugin +cd nemoclaw +npm run build # Compile TypeScript to JavaScript (outputs to dist/) + +# Development mode (watch for changes) +npm run dev # Run TypeScript compiler in watch mode + +# Clean build artifacts +npm run clean # Remove dist/ directory +``` + +### Top-Level Commands + +```bash +# Check both TypeScript and Python (runs linters + type checks) +make check + +# Lint both TypeScript and Python +make lint + +# Format all code (auto-fix) +make format +``` + +--- + +## Test Commands + +### Unit Tests + +```bash +# Run all tests (Node.js test runner with parallel execution) +npm test # Runs with concurrency=4 for speed + +# Run tests serially (for debugging) +npm run test:serial # Runs one test at a time + +# Run specific test file +node --test test/cli.test.js # Run specific test file +``` + +**Test Files Location**: `test/*.test.js` + +**Test Framework**: Node.js built-in test runner (node:test) + +**Test Isolation**: Tests run in parallel with concurrency=4 by default, ensuring: +- Each test file runs independently +- Tests don't share state between files +- Faster execution (4x speedup on multi-core systems) +- Use `npm run test:serial` if debugging intermittent failures + +**Important**: Tests verify CLI behavior, preflight checks, policy management, and NIM integration. + +--- + +## Linting and Formatting + +### TypeScript (nemoclaw/) + +```bash +cd nemoclaw + +# Run ESLint +npm run lint # Check for linting errors +npm run lint:fix # Auto-fix linting errors + +# Run Prettier +npm run format:check # Check code formatting +npm run format # Auto-format code + +# Run all checks (lint + format + type check) +npm run check # Run full validation suite +``` + +**Enforced Conventions**: +- **Naming**: camelCase for variables/functions, PascalCase for types/classes (via `@typescript-eslint/naming-convention`) +- **Strict TypeScript**: Full strict mode enabled +- **No unused vars**: Enforced (prefix with `_` for intentionally unused) +- **Complexity Limits**: Functions must have cyclomatic complexity ≤ 15, max depth ≤ 4, max lines ≤ 150 + +### Python (nemoclaw-blueprint/) + +```bash +cd nemoclaw-blueprint + +# Run Ruff linter +make check # Check for linting errors +ruff check . # Alternative: direct ruff command + +# Run Ruff formatter +make format # Auto-format and fix +ruff format . # Alternative: format only +``` + +**Enforced Conventions**: +- **Naming**: snake_case for functions/variables, PascalCase for classes (PEP 8 via `pep8-naming`) +- **Line length**: 100 characters +- **Import order**: Enforced via isort +- **Security**: flake8-bandit rules enabled +- **Complexity Limit**: Functions must have cyclomatic complexity ≤ 15 (McCabe via Ruff C90) + +### Pre-commit Hooks + +Automatically run on every commit: + +```bash +# Run all pre-commit hooks manually +pre-commit run --all-files + +# Update hooks to latest versions +pre-commit autoupdate +``` + +**Hooks include**: +- Trailing whitespace removal +- YAML/JSON validation +- ESLint (TypeScript) +- Prettier (TypeScript) +- Ruff (Python linter + formatter) +- TypeScript type checking +- Secret detection (detect-secrets) +- Large file detection (>1MB) + +--- + +## Documentation + +### Build Documentation + +```bash +# Build HTML documentation (Sphinx) +make docs # Build to docs/_build/html + +# Build and serve with live reload +make docs-live # Auto-rebuilds on changes, opens browser + +# Clean documentation build +make docs-clean # Remove docs/_build + +# Generate TypeScript API documentation +cd nemoclaw +npm run docs # Generate to nemoclaw/docs/api using TypeDoc +``` + +**Documentation Technologies**: +- **Sphinx** with MyST parser (Markdown support) - Main documentation +- **Sphinx Autodoc** - Automatic Python API documentation from docstrings +- **TypeDoc** - Automatic TypeScript API documentation from code comments +- **GitHub Actions** - Automated doc building on every push (`.github/workflows/docs.yml`) +- **Droid Skill** - `update-docs-from-commits` skill for AI-powered doc updates + +**Documentation URLs**: +- Sphinx docs: `docs/_build/html/index.html` +- TypeScript API docs: `nemoclaw/docs/api/index.html` + +**Automated Documentation Generation**: +1. **Python API docs**: Run `make docs` - Sphinx autodoc extracts from docstrings +2. **TypeScript API docs**: Run `cd nemoclaw && npm run docs` - TypeDoc generates from TSDoc comments +3. **Doc updates from commits**: Use skill `update-docs-from-commits` via droid +4. **CI validation**: GitHub Actions builds docs on every push, uploads artifacts + +**AGENTS.md Validation**: + +This file (AGENTS.md) is automatically validated to ensure accuracy: + +```yaml +# .github/workflows/docs-validation.yml +- Validates file paths referenced in AGENTS.md exist +- Verifies npm scripts are defined (test, build, lint, etc.) +- Checks Makefile targets exist (dev, check, lint, etc.) +- Validates skills are properly formatted with YAML frontmatter +- Tests that build commands actually work +- Runs markdown link checker +- Executes on every push to main and in PRs +``` + +**What gets validated:** +- ✅ All file paths mentioned in code blocks or documentation +- ✅ All npm scripts documented in commands sections +- ✅ All Makefile targets referenced +- ✅ Skills directory structure and frontmatter format +- ✅ Build, test, and lint commands are executable +- ✅ Markdown links (external links checked with retries) + +**Run validation locally:** +```bash +# GitHub Actions workflow runs automatically on push +# To test locally, check file existence manually: +ls bin/nemoclaw.js bin/lib/*.js +ls nemoclaw/src/*.ts +ls test/*.test.js + +# Verify npm scripts exist +grep "\"test\":" package.json +grep "\"build\":" nemoclaw/package.json + +# Verify Makefile targets +grep "^dev:" Makefile +grep "^check:" Makefile +``` + +**Why this matters:** +- Ensures documentation stays synchronized with code +- Catches broken references when files are moved/renamed +- Validates commands still work after dependency updates +- Prevents stale documentation from misleading agents +- Maintains trust in AGENTS.md as source of truth + +**CI Workflow**: `.github/workflows/docs-validation.yml` + +--- + +## Key Conventions + +### Code Style + +#### TypeScript +- **Strict TypeScript**: `strict: true` in tsconfig.json +- **Naming**: + - Variables/functions: `camelCase` + - Classes/interfaces/types: `PascalCase` + - Constants: `UPPER_CASE` or `camelCase` +- **Imports**: Use `type` imports for types (`import type { ... }`) +- **Async**: No floating promises (enforced by ESLint) + +#### Python +- **PEP 8 Compliance**: Enforced via Ruff's pep8-naming +- **Naming**: + - Functions/variables: `snake_case` + - Classes: `PascalCase` + - Constants: `SCREAMING_SNAKE_CASE` +- **Line length**: 100 characters +- **Docstrings**: Encouraged for public APIs + +### Git Workflow + +- **Commits**: Must be signed off (`git commit -s`) +- **Branches**: Protected - cannot commit directly to `main` +- **Pre-commit**: Hooks must pass before commit +- **Messages**: Descriptive commit messages with context +- **Code Ownership**: Automatic reviewer assignment via CODEOWNERS + - TypeScript changes: `@NVIDIA/typescript-reviewers` + - Python changes: `@NVIDIA/python-reviewers` + - Security files: `@NVIDIA/security-team` + - CI/CD: `@NVIDIA/devops-team` + - Documentation: `@NVIDIA/docs-team` + - All changes require `@NVIDIA/nemoclaw-maintainers` approval + +### Issue Reporting + +When creating issues, GitHub will prompt you to select the appropriate template: + +- **Bug Report**: Reproducible bugs or unexpected behavior +- **Feature Request**: Suggestions for new features or enhancements +- **Documentation**: Incorrect, missing, or unclear documentation +- **Security** (Public): Low-severity security improvements only + - For serious vulnerabilities, use the private reporting process in SECURITY.md + +Templates ensure you provide all necessary context for maintainers and autonomous agents to understand and address the issue effectively. + +### Pull Request Template + +When opening a pull request, GitHub automatically loads `.github/pull_request_template.md` with structured sections: + +- **Description**: What changed and why +- **Type of Change**: Bug fix, feature, breaking change, etc. +- **Testing Done**: Manual testing, automated tests, linting results +- **Security Considerations**: Critical checklist for secrets/credentials review +- **Breaking Changes**: Impact and migration path +- **Dependencies**: New or updated dependencies with justification +- **Pre-Submission Checklist**: Code quality, git hygiene, agent readability + +**For Autonomous Agents**: The template includes specific guidance on providing detailed testing evidence, redacting sensitive information, and ensuring changes are understandable by future AI contributors. All checkboxes must be appropriately marked with evidence. + +### File Organization + +- **TypeScript source**: `nemoclaw/src/` +- **Python source**: `nemoclaw-blueprint/` +- **Tests**: `test/` (unit tests with `.test.js` extension) +- **Build output**: `nemoclaw/dist/` (gitignored, except in npm package) + +--- + +## Common Tasks + +### Add a New CLI Command + +1. Create command file: `nemoclaw/src/commands/my-command.ts` +2. Implement command handler function +3. Register in `nemoclaw/src/cli.ts` +4. Build: `cd nemoclaw && npm run build` +5. Test: Verify command works via `node bin/nemoclaw.js my-command` + +### Modify Blueprint Logic + +1. Edit Python files in `nemoclaw-blueprint/orchestrator/` +2. Run linter: `cd nemoclaw-blueprint && make check` +3. Format: `make format` +4. Test integration manually with `nemoclaw onboard` + +### Update Documentation + +1. Edit Markdown files in `docs/` +2. Build docs: `make docs-live` (auto-reloads on changes) +3. Verify rendering in browser +4. Commit changes + +### Add Environment Variable + +1. Document in `.env.example` with description +2. Add usage in code (`process.env.VAR_NAME`) +3. Update AGENTS.md (this file) if critical +4. Document in README Security section if sensitive + +--- + +## Troubleshooting + +### TypeScript Compilation Errors + +```bash +# Check for type errors without building +cd nemoclaw +npx tsc --noEmit + +# Check specific file +npx tsc --noEmit src/commands/my-file.ts +``` + +### Pre-commit Hook Failures + +```bash +# Skip hooks temporarily (NOT RECOMMENDED) +git commit --no-verify + +# Fix issues automatically +pre-commit run --all-files + +# Debug specific hook +pre-commit run eslint --all-files --verbose +``` + +### ESLint/Prettier Conflicts + +Prettier is already integrated with ESLint (`eslint-config-prettier`). If conflicts occur: +1. Format with Prettier first: `npm run format` +2. Then fix ESLint issues: `npm run lint:fix` + +### Python Import Errors + +```bash +# Ensure dependencies are installed +cd nemoclaw-blueprint +uv sync # or: pip install -e . + +# Check Python version +python3 --version # Should be 3.11+ +``` + +--- + +## Testing Strategy + +- **Unit Tests**: Node.js test runner in `test/` directory +- **Integration**: Manual testing via `nemoclaw onboard` command +- **Pre-commit**: Automated quality checks before commit +- **CI**: (To be implemented - see failing signals) + +--- + +## Security Notes + +- **Never commit secrets**: `.env` files are gitignored and blocked by pre-commit hooks +- **API Keys**: Store in `.env`, never hardcode +- **Credentials**: Use `bin/lib/credentials.js` for persistent storage (mode 600) +- **SSH Keys**: Automatically excluded by comprehensive `.gitignore` +- **Dependency Updates**: Dependabot automatically creates PRs for dependency updates every Monday + - Configuration: `.github/dependabot.yml` + - Covers: npm (TypeScript), pip (Python), GitHub Actions, Docker + - Grouped updates to reduce PR noise + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| **Setup dev environment (one command)** | `make dev` | +| Install dependencies (TS) | `cd nemoclaw && npm install` | +| Install dependencies (Py) | `uv sync --group docs` | +| Build TypeScript | `cd nemoclaw && npm run build` | +| Run tests (unit, parallel) | `npm test` | +| Run tests (all: unit + integration) | `npm run test:all` | +| Run tests (integration only) | `npm run test:integration` | +| Run tests (serial/debug) | `npm run test:serial` | +| Run tests (with timing) | `npm run test:timing` | +| Run tests (with coverage) | `npm run test:coverage` | +| Lint everything | `make lint` | +| Format everything | `make format` | +| Check everything | `make check` | +| Complexity analysis | `make complexity` | +| Dead code detection | `make dead-code` | +| Duplicate code detection | `make duplicates` | +| Technical debt tracking | `make tech-debt` | +| Build docs | `make docs` | +| Run pre-commit checks | `pre-commit run --all-files` | +| Install pre-commit hooks | `pre-commit install` | + +--- + +## Feature Flags + +NemoClaw uses feature flags to enable safe rollout of experimental features. This is critical for autonomous agents shipping changes incrementally. + +**Available flags:** + +- `NEMOCLAW_EXPERIMENTAL=1`: Enable all experimental features (local inference, new endpoints) +- `NEMOCLAW_LOCAL_INFERENCE=1`: Enable local inference endpoints only (NIM, vLLM, Ollama) +- `NEMOCLAW_AUTO_SELECT=1`: Auto-select detected providers during onboarding +- `NEMOCLAW_VERBOSE=1`: Enable verbose debug logging + +**Check flag status:** + +```bash +nemoclaw feature-flags # Show all flags and their current state +``` + +**Full documentation:** [docs/feature-flags.md](docs/feature-flags.md) + +**For agents:** When implementing new features, ship them behind feature flags to reduce risk. See the feature flags documentation for how to add new flags. + +--- + +## Release Notes and Changelog + +NemoClaw uses automated release notes generation to document all changes, including agent contributions. + +**Automated systems:** +- **GitHub Actions**: Auto-generates release notes on version tags +- **CHANGELOG.md**: Automatically updated with categorized changes +- **Manual generation**: `npm run changelog` for local updates + +**Commit message conventions** (important for agents): +- `feat:` → Listed under Features in release notes +- `fix:` → Listed under Bug Fixes +- `docs:` → Listed under Documentation +- `security:` → Listed under Security + +**Create a release:** +```bash +git tag -a v0.2.0 -m "Release v0.2.0" +git push origin v0.2.0 +# GitHub Actions automatically generates release notes and updates CHANGELOG.md +``` + +**Full documentation:** [docs/releases.md](docs/releases.md) + +--- + +## Deployment and Release Automation + +NemoClaw has fully automated release pipelines that publish Docker images and npm packages. + +**Automated workflows:** +- **Docker images**: Published to GitHub Container Registry (`ghcr.io/nvidia/nemoclaw`) +- **npm packages**: Published to npm registry (`nemoclaw` package) +- **SBOM generation**: Software Bill of Materials attached to releases + +**Create a release:** +```bash +npm version minor # Updates version and creates tag +git push origin main --tags +# Automation handles: release notes, Docker publish, npm publish, CHANGELOG update +``` + +**Using published artifacts:** +```bash +# Docker +docker pull ghcr.io/nvidia/nemoclaw:latest + +# npm +npm install -g nemoclaw +``` + +**Full documentation:** [docs/deployment.md](docs/deployment.md) + +--- + +## Observability and Logging + +NemoClaw uses **structured logging** with [pino](https://getpino.io/), **distributed tracing**, **metrics collection**, and **error tracking** (Sentry) for comprehensive observability and debugging. + +### Structured Logging + +**Logger module**: `bin/lib/logger.js` + +**Basic usage:** +```javascript +const { logger } = require("./lib/logger"); + +// Structured logging with context (trace context added automatically) +logger.info({ sandbox: "my-sandbox", model: "nemotron" }, "Inference completed"); +logger.error({ err: error, operation: "connect" }, "Failed to connect"); + +// Convenience functions +const { logCommand, logSandboxOperation } = require("./lib/logger"); +logCommand("onboard", { profile: "vllm" }); +logSandboxOperation("my-sandbox", "start", { duration: 1500 }); +``` + +**Log levels:** +- `logger.trace()` - Very detailed debug (enabled with `NEMOCLAW_LOG_LEVEL=trace`) +- `logger.debug()` - Debug information (enabled with `NEMOCLAW_VERBOSE=1`) +- `logger.info()` - General information (default) +- `logger.warn()` - Warnings +- `logger.error()` - Errors +- `logger.fatal()` - Fatal errors + +**Configuration:** +```bash +export NEMOCLAW_VERBOSE=1 # Enable verbose debug logging +export NEMOCLAW_LOG_LEVEL=debug # Set specific log level +export NODE_ENV=production # JSON output (vs pretty print) +``` + +### Distributed Tracing + +**Trace context module**: `bin/lib/trace-context.js` + +Every CLI command automatically gets a unique trace ID that propagates through all operations. + +**Usage:** +```javascript +const { runWithTraceContext, getTraceId, getTraceHeaders } = require("./lib/trace-context"); + +// Run operation within trace context +await runWithTraceContext("sandbox-create", async () => { + logger.info({ sandbox: "test" }, "Creating sandbox"); + // traceId is automatically included in logs +}, { sandbox: "test" }); + +// Get current trace ID +const traceId = getTraceId(); + +// Add trace headers to HTTP requests +const headers = { ...getTraceHeaders() }; +// Adds: X-Request-ID, X-Trace-ID, X-Span-ID +``` + +**Debugging with trace IDs:** +```bash +# All logs for a specific CLI invocation +nemoclaw onboard 2>&1 | jq 'select(.traceId == "...")' + +# Show operation timeline +nemoclaw onboard 2>&1 | jq -s 'sort_by(.time) | .[] | {time, operation: .traceOperation, duration}' +``` + +### Metrics Collection + +**Metrics module**: `bin/lib/metrics.js` + +All CLI commands automatically collect performance metrics (duration, error rates, etc.). + +**Usage:** +```javascript +const { recordCommandExecution, recordInferenceRequest, startTimer } = require("./lib/metrics"); + +// Record command execution +recordCommandExecution("onboard", duration, { status: "success" }); + +// Record inference request +recordInferenceRequest("nvidia/nemotron", 420, { tokens: 150, cached: false }); + +// Time an operation +const timer = startTimer("nemoclaw.operation.duration", { operation: "create" }); +await performOperation(); +timer(); // Records duration automatically +``` + +**Built-in metrics:** +- `nemoclaw.command.executions` - Command invocations (counter) +- `nemoclaw.command.duration` - Command execution time (histogram) +- `nemoclaw.inference.requests` - Inference requests (counter) +- `nemoclaw.inference.latency` - Inference duration (histogram) +- `nemoclaw.sandbox.operations` - Sandbox operations (counter) +- `nemoclaw.errors` - Error count (counter) + +**Configuration:** +```bash +export NEMOCLAW_METRICS=0 # Disable metrics +export NEMOCLAW_METRICS_BACKEND=console # Use console backend (vs logger) +``` + +**Querying metrics:** +```bash +# Find all metrics +nemoclaw onboard 2>&1 | jq 'select(.metric_type)' + +# Calculate average command duration +nemoclaw onboard 2>&1 | jq -s '[.[] | select(.metric_name == "nemoclaw.command.duration") | .metric_value] | add / length' + +# Count errors +nemoclaw onboard 2>&1 | jq -s '[.[] | select(.metric_name == "nemoclaw.errors")] | length' +``` + +### Error Tracking (Sentry) + +**Sentry module**: `bin/lib/sentry.js` + +Production error tracking with Sentry (opt-in via SENTRY_DSN). + +**Features:** +- **Source maps**: TypeScript stack traces in production +- **Breadcrumbs**: Timeline of events before errors +- **Trace context**: Automatic trace ID correlation +- **User context**: Non-PII user identification + +**Setup:** +```bash +# .env file (get DSN from https://sentry.io/) +export SENTRY_DSN=https://abc123@o123456.ingest.sentry.io/789 +export SENTRY_ENVIRONMENT=production +``` + +**Usage:** +```javascript +const { captureException, addBreadcrumb } = require("./lib/sentry"); + +// Add breadcrumb +addBreadcrumb({ + category: "sandbox", + message: "Creating sandbox", + data: { sandbox: "my-sandbox" }, +}); + +// Capture error with context +try { + await createSandbox(name); +} catch (error) { + captureException(error, { + tags: { sandbox: name, operation: "create" }, + extra: { model: "nemotron", gpu: true }, + }); + throw error; +} +``` + +**Full documentation:** [docs/observability.md](docs/observability.md) + +### Deployment Observability + +**Monitoring dashboards**: Track deployment impact in real-time. + +NemoClaw integrates with major monitoring platforms for deployment observability: + +**Dashboard platforms:** +- **Datadog**: Create dashboard with `nemoclaw.command.duration`, `nemoclaw.errors` metrics +- **Grafana**: Query Prometheus metrics for error rates and latencies +- **New Relic**: Use NRQL to query transaction performance +- **CloudWatch**: Parse structured logs with Insights queries + +**Example queries:** +```bash +# Prometheus: Error rate +rate(nemoclaw_errors_total{env="production"}[5m]) + +# Datadog: Command latency +avg:nemoclaw.command.duration{env:production} by {command} + +# CloudWatch Insights: Error count +fields @timestamp, level, msg +| filter level = "error" +| stats count() by bin(5m) +``` + +**Deploy notifications:** +- Configure Slack/Discord/Teams webhooks +- Send notifications from CI/CD pipelines +- Include dashboard links in notifications + +**Deployment markers:** +```bash +# Datadog event API +curl -X POST "https://api.datadoghq.com/api/v1/events" \ + -H "DD-API-KEY: $DD_API_KEY" \ + -d '{"title": "NemoClaw deployed", "text": "v1.2.3", "tags": ["service:nemoclaw"]}' + +# Grafana annotation +curl -X POST "https://grafana.example.com/api/annotations" \ + -H "Authorization: Bearer $GRAFANA_API_KEY" \ + -d '{"text": "Deployed v1.2.3", "tags": ["deployment"]}' +``` + +**Dashboard links** (add your organization's dashboards): +```markdown +- Metrics: https://app.datadoghq.com/dashboard/[your-id] +- Errors: https://sentry.io/organizations/[org]/projects/nemoclaw/ +- Logs: https://your-logging-platform.com +``` + +**Deployment health check:** +```bash +# Monitor error rate after deployment +nemoclaw 2>&1 | jq 'select(.level >= 50)' | jq -s 'length' + +# Check command duration +nemoclaw 2>&1 | jq -s '[.[] | select(.metric_name == "nemoclaw.command.duration") | .metric_value] | add / length' +``` + +**Full documentation:** [docs/observability.md](docs/observability.md#deployment-observability) + +--- + +## Incident Response and Runbooks + +**Runbook documentation**: [docs/runbooks.md](docs/runbooks.md) + +NemoClaw provides incident response playbooks for common production issues. + +**Runbook categories:** +1. **General Troubleshooting**: Commands, logs, monitoring checks +2. **Sandbox Incidents**: Creation failures, startup issues, inference errors +3. **Inference Incidents**: Performance, invalid responses, API issues +4. **Deployment Incidents**: Error spikes, failed deploys, rollbacks +5. **Performance Incidents**: Memory, CPU, resource exhaustion +6. **Security Incidents**: API key leaks, unauthorized access +7. **Escalation Procedures**: When and how to escalate + +**Quick diagnostic commands:** + +```bash +# System status +nemoclaw status # Sandbox and service status +nemoclaw list # List all sandboxes +docker ps # Check containers + +# Recent errors +nemoclaw 2>&1 | jq 'select(.level >= 50)' + +# Command performance +nemoclaw 2>&1 | jq -s '[.[] | select(.metric_name == "nemoclaw.command.duration") | .metric_value] | add / length' + +# Trace specific request +export NEMOCLAW_VERBOSE=1 +nemoclaw 2>&1 | jq 'select(.traceId == "TRACE_ID")' +``` + +**Common incidents:** + +**Sandbox creation fails:** +```bash +# Check Docker +docker ps && docker info + +# Check OpenShell +which openshell && openshell --version + +# Check disk space +df -h # Need at least 5GB free +``` + +**Inference requests failing:** +```bash +# Verify API key +echo $NVIDIA_API_KEY | head -c 10 + +# Test API directly +curl https://api.nvidia.com/v1/health \ + -H "Authorization: Bearer $NVIDIA_API_KEY" +``` + +**Deployment caused errors:** +```bash +# Check error rate increase +# If >2x baseline: ROLLBACK IMMEDIATELY +git checkout +npm install && npm run build + +# Notify team +curl -X POST "$SLACK_WEBHOOK_URL" \ + -d '{"text": "⚠️ Rolled back due to error spike"}' +``` + +**Escalation:** +- **Level 1**: On-call engineer (Slack: #nemoclaw-oncall, PagerDuty) +- **Level 2**: Team lead +- **Level 3**: Engineering manager + +**Escalate immediately if:** +- Production down >15 minutes +- Security incident detected +- Data loss suspected +- Unable to resolve within 30 minutes + +**Full runbooks:** [docs/runbooks.md](docs/runbooks.md) + +### Alerting + +**Alert configuration**: [docs/observability.md](docs/observability.md#alerting) + +NemoClaw provides recommended alert rules for production monitoring. + +**Critical alerts** (page on-call): +1. **High error rate**: > 10 errors/minute for 5 minutes +2. **Service down**: No metrics received for 5 minutes +3. **Inference API down**: Success rate < 50% for 5 minutes + +**Warning alerts** (notify channel): +1. **Elevated errors**: > 5 errors/minute for 10 minutes +2. **High latency**: p95 > 5s for 10 minutes +3. **High memory**: > 80% for 15 minutes + +**Alert integrations:** +- **PagerDuty**: For critical alerts (error spikes, service down) +- **OpsGenie**: Alternative to PagerDuty +- **Slack**: For warning/info alerts (#nemoclaw-alerts, #nemoclaw-deploys) + +**Example alert queries:** + +**Prometheus:** +```promql +# Error rate alert +rate(nemoclaw_errors_total{env="production"}[5m]) > 10/60 + +# Latency alert +histogram_quantile(0.95, rate(nemoclaw_command_duration_bucket[5m])) > 5 +``` + +**Datadog:** +``` +# Error rate +avg(last_5m):avg:nemoclaw.errors{env:production}.as_rate() > 0.16 + +# Latency +avg(last_10m):p95:nemoclaw.command.duration{env:production} > 5000 +``` + +**CloudWatch:** +```sql +# Error count +SELECT COUNT(*) FROM Logs WHERE level='error' | COUNT > 50 +``` + +**Alert best practices:** +- Alert on symptoms (error rate), not causes (disk space) +- Include runbook links in notifications +- Set appropriate thresholds to reduce false positives +- Test alerts monthly + +**Full alert documentation:** [docs/observability.md](docs/observability.md#alerting) + +### Product Analytics + +**Analytics documentation**: [docs/product-analytics.md](docs/product-analytics.md) + +NemoClaw supports optional product analytics to measure feature usage and impact. + +**Platform**: Post Hog (recommended for CLI tools) +- Open source, can be self-hosted +- Privacy-focused, GDPR compliant +- Good for measuring feature adoption + +**Setup** (opt-in only): +```bash +# .env file +POSTHOG_API_KEY=phc_your_api_key_here +POSTHOG_HOST=https://app.posthog.com +``` + +**What to track:** +- **Command execution**: Which commands users run, duration, success rate +- **Feature usage**: Feature flag adoption, feature retention +- **Sandbox operations**: Creation success rate, model selection +- **Inference requests**: Token usage, latency, caching effectiveness +- **Errors**: Error types, operation failures + +**What NOT to track:** +- Never track API keys, credentials, or secrets +- Never track user code, prompts, or sandbox names +- Never track PII (unless anonymized/hashed) + +**For autonomous agents:** + +When adding a new feature: +1. Add analytics tracking: `trackFeature('feature_name', {context})` +2. Monitor adoption after 7 days (PostHog dashboard) +3. Make data-driven decisions: + - High adoption + low errors = keep feature + - Low adoption + high errors = improve or deprecate + - High adoption + high errors = fix urgently + +**Example analytics integration:** +```javascript +const { initAnalytics, trackCommand } = require('./lib/analytics'); + +// Initialize (only if POSTHOG_API_KEY set) +initAnalytics(); + +// Track command +const startTime = Date.now(); +await executeCommand(cmd); +const duration = Date.now() - startTime; +trackCommand(cmd, duration, 'success'); +``` + +**Measuring feature impact:** +```bash +# Query PostHog API for feature adoption +curl https://app.posthog.com/api/projects/$PROJECT_ID/insights/trend \ + -H "Authorization: Bearer $POSTHOG_API_KEY" \ + -d '{"events": [{"id": "feature_used", "properties": [{"key": "feature", "value": "new_feature"}]}]}' +``` + +**Privacy:** +- Anonymous by default (hashed user IDs) +- Opt-out: `export NEMOCLAW_TELEMETRY=0` +- GDPR compliant with data retention policies +- Self-hosting option for sensitive environments + +**Full documentation:** [docs/product-analytics.md](docs/product-analytics.md) + +### Error to Insight Pipeline + +**Pipeline documentation**: [docs/error-to-insight-pipeline.md](docs/error-to-insight-pipeline.md) + +The error-to-insight pipeline automatically converts production errors into GitHub issues. + +**Flow:** +``` +Production Error → Sentry → GitHub Issue → Fix → Deploy → Verify +``` + +**Setup:** +```bash +# .env file +SENTRY_ORG=your-organization-slug +SENTRY_PROJECT=nemoclaw +``` + +**Sentry-GitHub integration:** +1. Install GitHub integration in Sentry (Settings → Integrations → GitHub) +2. Configure alert rules (Alerts → Create Alert Rule) +3. Set trigger: "First seen" for new errors +4. Set action: "Create GitHub issue" +5. Configure issue template (title, labels, assignees) + +**Alert rules** (recommended): +- **New errors**: Create issue for first-seen errors in production +- **High volume**: Create issue for >100 events/hour +- **Regressions**: Create issue when resolved error recurs +- **High impact**: Create issue + page on-call for >50 users affected + +**Example GitHub issue** (auto-created): +```markdown +## [Sentry] ConnectionError: Failed to connect to inference API + +Events: 42 +Users Affected: 12 +Stack Trace: [link] +Sentry: [view full error] + +Labels: bug, sentry, production +``` + +**For autonomous agents:** + +When Sentry creates an issue: +1. Query Sentry API for unresolved errors +2. Prioritize by user impact (`userCount`) +3. Analyze stack trace and breadcrumbs +4. Implement fix +5. Commit with `Fixes #[issue-number]` +6. Verify error resolved in Sentry after deployment + +**Query errors:** +```bash +# Get recent unresolved errors +curl https://sentry.io/api/0/projects/$SENTRY_ORG/$SENTRY_PROJECT/issues/ \ + -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ + -G --data-urlencode "query=is:unresolved" \ + | jq 'sort_by(.userCount) | reverse | .[] | {title, userCount, permalink}' +``` + +**Fix workflow:** +```bash +# 1. Get top error by user impact +# 2. Analyze stack trace +# 3. Implement fix +# 4. Commit with issue reference +git commit -m "Fix inference connection timeout + +Fixes #456 + +- Increase timeout to 60s +- Add retry logic" + +# 5. After deploy, verify in Sentry +``` + +**Benefits:** +- Automatic issue creation (no manual copying) +- Rich error context in GitHub +- Deduplication (one issue per error group) +- Workflow integration +- Automatic issue closure when fixed + +**Metrics to track:** +- Time to triage (error → issue assigned) +- Time to fix (issue created → PR merged) +- Time to resolve (PR merged → error resolved) +- Regression rate (% errors that recur) + +**Full documentation:** [docs/error-to-insight-pipeline.md](docs/error-to-insight-pipeline.md) + +--- + +## Repository Skills + +NemoClaw provides reusable skills (automation capabilities) for common development tasks. Skills are located in `.factory/skills/` and follow the Claude skills standard. + +**Available Skills:** + +1. **run-full-test-suite** - Execute complete test suite with coverage + - Run all unit and integration tests + - Generate coverage reports + - Check test performance + - Location: `.factory/skills/run-full-test-suite/SKILL.md` + +2. **lint-and-format-code** - Lint and format TypeScript and Python + - Auto-fix ESLint and Ruff issues + - Apply Prettier and Ruff formatting + - Verify pre-commit checks pass + - Location: `.factory/skills/lint-and-format-code/SKILL.md` + +3. **check-code-quality** - Analyze code quality metrics + - Check cyclomatic complexity + - Detect dead code (knip, vulture) + - Find duplicate code (jscpd) + - Track technical debt (TODO/FIXME) + - Location: `.factory/skills/check-code-quality/SKILL.md` + +4. **build-project** - Build TypeScript plugin + - Compile TypeScript to JavaScript + - Verify type checking passes + - Generate type definitions + - Location: `.factory/skills/build-project/SKILL.md` + +5. **generate-release-notes** - Create changelog from commits + - Generate release notes automatically + - Update CHANGELOG.md + - Follow conventional commits + - Location: `.factory/skills/generate-release-notes/SKILL.md` + +6. **update-docs-from-commits** - Sync docs with code changes + - Scan git commits for user-facing changes + - Identify affected documentation pages + - Draft documentation updates + - Location: `.agents/skills/update-docs-from-commits/SKILL.md` + +**Using Skills:** + +Skills provide step-by-step instructions for common tasks. Read the SKILL.md file for: +- When to use the skill +- Prerequisites and commands +- Best practices and examples +- Troubleshooting and success criteria + +**Example:** +```bash +# To run the full test suite, see: +cat .factory/skills/run-full-test-suite/SKILL.md + +# To check code quality, see: +cat .factory/skills/check-code-quality/SKILL.md +``` + +--- + +## Architecture Documentation + +**Visual architecture diagrams** are available in `docs/architecture/` with comprehensive documentation: + +1. **[System Overview](docs/architecture/system-overview.mermaid)** - Complete component architecture showing CLI → Plugin → Blueprint → Gateway → Inference flow with all major components and their relationships + +2. **[Onboarding Flow](docs/architecture/onboarding-flow.mermaid)** - Detailed sequence diagram of the onboarding process including preflight checks, blueprint execution, sandbox deployment, and inference configuration + +3. **[Inference Routing](docs/architecture/inference-routing.mermaid)** - Data flow diagram showing request routing, provider selection (NVIDIA Cloud, NIM, vLLM, Ollama), caching, and observability integration + +4. **[Component Interactions](docs/architecture/component-interactions.mermaid)** - Code organization showing dependencies between bin/, nemoclaw/src/, nemoclaw-blueprint/, and test/ directories + +5. **[Deployment Model](docs/architecture/deployment-model.mermaid)** - Runtime architecture showing npm installation, Docker containers, inference infrastructure, and persistent storage locations + +**See [docs/architecture/README.md](docs/architecture/README.md)** for: +- How to view and export diagrams +- Architecture principles and design patterns +- External dependencies and data flows +- Security and performance characteristics +- Extension guides for adding commands/providers/blueprints + +**Key concepts** (detailed in diagrams): +- **Three-layer architecture**: Presentation (CLI/Plugin) → Business Logic (Blueprint) → Infrastructure (OpenShell) +- **Blueprint lifecycle**: Resolve → Verify → Plan → Apply +- **Inference routing**: Gateway routes requests to NVIDIA Cloud, NIM, vLLM, or Ollama based on configuration +- **Observability**: All operations instrumented with logs, traces, metrics, and error tracking + +--- + +## Additional Resources + +- **README.md**: User-facing documentation and quick start +- **CONTRIBUTING.md**: Contribution guidelines and code style details +- **SECURITY.md**: Security vulnerability reporting +- **docs/**: Full Sphinx documentation (build with `make docs`) +- **docs/architecture/**: Architecture diagrams and design documentation +- **docs/feature-flags.md**: Complete feature flag documentation +- **docs/releases.md**: Release notes and changelog automation +- **.env.example**: Environment variable template and documentation + +--- + +## Project-Specific Knowledge + +### Blueprint Lifecycle + +The Python blueprint follows a 4-stage lifecycle: +1. **Resolve**: Locate and verify blueprint version +2. **Verify**: Check artifact digest for integrity +3. **Plan**: Determine OpenShell resources to create +4. **Apply**: Execute plan via OpenShell CLI + +### Sandbox Architecture + +- **OpenShell Gateway**: Routes inference calls +- **Sandbox Container**: Isolated environment for OpenClaw +- **Policy**: Network egress and filesystem access controls +- **Inference**: NVIDIA cloud API calls routed through gateway + +### Key Files to Understand + +- `nemoclaw/src/index.ts`: Plugin registration and initialization +- `nemoclaw-blueprint/orchestrator/runner.py`: Main blueprint orchestration +- `bin/nemoclaw.js`: CLI dispatcher (all commands route through here) +- `bin/lib/onboard.js`: Onboarding wizard implementation +- `.pre-commit-config.yaml`: Quality enforcement configuration + +--- + +**Last Updated**: 2026-03-22 +**Maintained by**: NVIDIA NemoClaw Team +**For Agent Questions**: Refer to this file first, then README.md, then source code comments + +--- + +## Autonomous Agent Operation + +For AI agents that need to run without human interruption: + +### Quick Start: Observe Agent Without Interrupting + +**Problem:** Agent keeps asking "what to do next?" and you just want to observe. + +**Solution:** +``bash +# 1. Open dashboard in browser +# http://127.0.0.1:18789 + +# 2. Stream logs in terminal +nemoclaw my-assistant logs --follow | jq -C '.' + +# 3. Configure agent for autonomous operation +cat > ~/.openclaw/openclaw.json <` - Session identifier (maintains context across calls) + +### When to Use + +✅ **Good for:** +- Running specific tasks programmatically +- Batch processing +- Automated testing +- Scheduled jobs (cron) + +❌ **Not good for:** +- Continuous operation +- Multi-turn conversations requiring context +- Real-time interactive debugging + +--- + +## Method 2: TUI Mode with Auto-Messages + +Use the OpenClaw TUI (Terminal User Interface) with automatic message sending: + +### Usage + +```bash +# Start TUI +openclaw tui + +# Or connect from outside sandbox +nemoclaw my-assistant connect -- openclaw tui +``` + +### Configuration + +Edit `~/.openclaw/openclaw.json` to configure TUI behavior: + +```json +{ + "tui": { + "autoSubmit": true, + "autoSubmitDelay": 2000, + "defaultMessage": "continue with the next task", + "confirmBeforeAction": false + } +} +``` + +**Options:** +- `autoSubmit` - Automatically send messages without Enter key +- `autoSubmitDelay` - Delay in milliseconds before auto-submit +- `defaultMessage` - Message to send when user provides no input +- `confirmBeforeAction` - Skip confirmation prompts + +### When to Use + +✅ **Good for:** +- Watching agent work in real-time +- Development and debugging +- Demonstrations + +❌ **Not good for:** +- Background tasks +- Headless servers +- Production automation + +--- + +## Method 3: Gateway Control UI (Best for Observation) + +Use the built-in web dashboard to monitor and control the agent: + +### Access Dashboard + +```bash +# Get dashboard URL +cat ~/.openclaw/openclaw.json | jq -r '.gateway.auth.token' | \ + xargs -I {} echo "http://127.0.0.1:18789/#token={}" + +# Or check logs +cat /tmp/gateway.log | grep "Local UI" +``` + +### Configuration for Autonomous Operation + +Edit `~/.openclaw/openclaw.json`: + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "nvidia/nemotron-3-super-120b-a12b" + }, + "behavior": { + "confirmBeforeAction": false, + "autonomousMode": true, + "maxIterationsWithoutHuman": 50, + "taskCompletionBehavior": "continue" + } + } + }, + "commands": { + "native": "auto", + "nativeSkills": "auto", + "restart": false, + "confirmExecution": false + } +} +``` + +**Key Settings:** +- `confirmBeforeAction: false` - Don't ask before executing commands +- `autonomousMode: true` - Enable autonomous agent behavior +- `maxIterationsWithoutHuman: 50` - How many steps before pausing +- `taskCompletionBehavior: "continue"` - What to do after completing a task +- `restart: false` - Don't restart after each command +- `confirmExecution: false` - Execute commands without confirmation + +### When to Use + +✅ **Good for:** +- Long-running autonomous tasks +- Background processing +- Production agents +- Continuous operation + +--- + +## Method 4: Channel-Based Automation + +Use Telegram, Slack, or Discord channels for asynchronous agent interaction: + +### Enable Telegram Bot + +```bash +# Set bot token +export TELEGRAM_BOT_TOKEN="your-bot-token" + +# Edit config +nano ~/.openclaw/openclaw.json +``` + +```json +{ + "channels": { + "telegram": { + "enabled": true, + "dmPolicy": "open", + "botToken": "${TELEGRAM_BOT_TOKEN}", + "allowFrom": ["*"], + "groupPolicy": "allowlist", + "streaming": "partial", + "autonomousMode": true + } + } +} +``` + +### Usage + +1. Message the Telegram bot +2. Agent processes message asynchronously +3. Responds when done +4. No interactive blocking! + +### When to Use + +✅ **Good for:** +- Remote access +- Team collaboration +- Asynchronous workflows +- Mobile access + +--- + +## Recommended Configuration for Your Use Case + +Based on your requirement: **"I just want to observe what is going on without interrupting"** + +### Step 1: Configure Agent for Autonomous Operation + +Create or edit `~/.openclaw/openclaw.json`: + +```json +{ + "agents": { + "defaults": { + "model": { + "primary": "nvidia/nemotron-3-super-120b-a12b" + }, + "behavior": { + "confirmBeforeAction": false, + "autonomousMode": true, + "maxIterationsWithoutHuman": 100, + "taskCompletionBehavior": "continue" + }, + "compaction": { + "mode": "safeguard" + } + } + }, + "commands": { + "native": "auto", + "nativeSkills": "auto", + "restart": false, + "confirmExecution": false, + "ownerDisplay": "raw" + }, + "gateway": { + "mode": "local", + "controlUi": { + "allowedOrigins": ["http://127.0.0.1:18789"], + "allowInsecureAuth": true, + "dangerouslyDisableDeviceAuth": true + }, + "auth": { + "mode": "token" + }, + "trustedProxies": ["127.0.0.1", "::1"] + } +} +``` + +### Step 2: Start Agent in Autonomous Mode + +```bash +# In sandbox, start agent with a continuous task +openclaw agent --agent main --local -m "monitor system health and report issues" --session-id autonomous-01 +``` + +### Step 3: Observe in Real-Time + +**Terminal 1: Dashboard (Browser)** +```bash +# Open http://127.0.0.1:18789 +# Watch agent activity in real-time +``` + +**Terminal 2: Live Logs** +```bash +# Stream structured logs +nemoclaw my-assistant logs --follow | jq -C '.' +``` + +**Terminal 3: Gateway Activity** +```bash +# Watch gateway events +tail -f /tmp/gateway.log +``` + +### Step 4: Give Agent Continuous Tasks + +Instead of one-off messages, give the agent ongoing responsibilities: + +```bash +# Example: Continuous monitoring +openclaw agent --agent main --local -m "continuously monitor the system: 1) check logs every 5 minutes, 2) report any errors, 3) suggest fixes when issues are found. Never stop monitoring." --session-id monitor-01 + +# Example: Process queue +openclaw agent --agent main --local -m "process all pending tasks in the queue. When the queue is empty, check again in 60 seconds. Continue forever." --session-id worker-01 + +# Example: Research task +openclaw agent --agent main --local -m "research the best practices for OpenClaw plugin development. Compile a comprehensive guide. Take your time and be thorough." --session-id research-01 +``` + +--- + +## Troubleshooting + +### Agent Stops After Each Response + +**Problem:** Agent completes task and waits for next instruction + +**Solution:** +1. Set `taskCompletionBehavior: "continue"` in config +2. Give explicit instructions to "continue" in your prompt +3. Use `--session-id` to maintain context + +### Agent Asks for Confirmation Before Commands + +**Problem:** Agent shows "Execute this command? (y/n)" + +**Solution:** +1. Set `confirmBeforeAction: false` in agent config +2. Set `confirmExecution: false` in commands config +3. Restart the agent for config to take effect + +### Agent Stops After N Iterations + +**Problem:** Agent pauses after X steps + +**Solution:** +1. Increase `maxIterationsWithoutHuman` in config +2. Set to high number (100+) for long-running tasks +3. Or set to `-1` for unlimited (use with caution!) + +### Can't See What Agent Is Doing + +**Problem:** No visibility into agent activity + +**Solution:** +1. Use dashboard: http://127.0.0.1:18789 +2. Stream logs: `nemoclaw logs --follow` +3. Enable verbose logging: `export NEMOCLAW_VERBOSE=1` +4. Check gateway logs: `tail -f /tmp/gateway.log` + +--- + +## Example: Complete Autonomous Setup + +Here's a complete script to set up autonomous agent observation: + +```bash +#!/bin/bash +# setup-autonomous-agent.sh + +set -e + +echo "Setting up autonomous agent configuration..." + +# 1. Configure OpenClaw for autonomous operation +cat > ~/.openclaw/openclaw.json < /tmp/agent-activity.log 2>&1 & +LOG_PID=$! +echo "✓ Log monitoring started (PID: $LOG_PID)" + +# 4. Start agent with autonomous task +echo "Starting autonomous agent..." +nemoclaw my-assistant connect -- \ + openclaw agent --agent main --local \ + -m "You are now in autonomous mode. Your task: continuously monitor system health, check logs for errors, and suggest improvements. Check logs every 5 minutes. Report findings. Never stop." \ + --session-id autonomous-$(date +%s) & + +AGENT_PID=$! +echo "✓ Agent started (PID: $AGENT_PID)" + +echo "" +echo "=== Autonomous Agent Running ===" +echo "Dashboard: http://127.0.0.1:18789/#token=$TOKEN" +echo "Live logs: tail -f /tmp/agent-activity.log" +echo "Gateway logs: tail -f /tmp/gateway.log" +echo "" +echo "To stop:" +echo " kill $AGENT_PID $LOG_PID" +``` + +Make it executable and run: + +```bash +chmod +x setup-autonomous-agent.sh +./setup-autonomous-agent.sh +``` + +--- + +## Best Practices for Autonomous Agents + +1. **Always specify session IDs** - Maintains context across interactions +2. **Set clear, continuous tasks** - Agent needs ongoing goals, not one-off tasks +3. **Use structured logging** - Makes observation easier +4. **Monitor via dashboard + logs** - Multiple observation points +5. **Set iteration limits** - Prevent runaway agents +6. **Test with short tasks first** - Verify autonomy before long-running tasks +7. **Use feature flags** - Enable/disable autonomous features safely + +--- + +## Security Considerations + +**Important:** Autonomous agents can execute commands without human approval. Ensure: + +1. **Sandbox isolation** - Run in OpenShell sandbox with policies +2. **Network policies** - Limit egress to approved endpoints +3. **Resource limits** - Set max iterations, timeouts +4. **Monitoring** - Always observe autonomous agents +5. **Kill switch** - Have a way to stop runaway agents + +**Example policy:** + +```yaml +# openclaw-sandbox.yaml +egress: + - endpoint: "https://build.nvidia.com" + allow: ["POST"] + - endpoint: "https://api.github.com" + allow: ["GET"] + +commands: + allowlist: + - curl + - git + - npm + blocklist: + - rm + - dd + - mkfs +``` + +--- + +## Summary + +To observe your agent without interruption: + +1. **Configure** `~/.openclaw/openclaw.json` with autonomous settings +2. **Open dashboard** at http://127.0.0.1:18789 +3. **Stream logs** with `nemoclaw logs --follow` +4. **Start agent** with continuous task using `openclaw agent --agent main --local -m "..." --session-id ...` +5. **Observe** via dashboard + logs without interrupting! + +The agent will now work autonomously while you observe all activity in real-time. From 27cf0940753a447a38e27252fefb6ffd3a3b584c Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:19:45 -0700 Subject: [PATCH 09/24] fix(policy): resolve streaming inference errors by using access: full Replaces tls: terminate with access: full for NVIDIA and Anthropic inference endpoints. TLS termination causes the proxy to fail decoding chunked streaming responses, resulting in "error decoding response body" warnings. Endpoints fixed: - integrate.api.nvidia.com - inference-api.nvidia.com - api.anthropic.com - statsig.anthropic.com - sentry.io This follows the same fix applied to GitHub/npm in commit 24a1b4e. access: full allows CONNECT tunneling to pass through without L7 inspection, which is required for streaming APIs. Fixes: upstream protocol error warnings in sandbox logs Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../policies/openclaw-sandbox.yaml | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/nemoclaw-blueprint/policies/openclaw-sandbox.yaml b/nemoclaw-blueprint/policies/openclaw-sandbox.yaml index 3e3d1cd92..dc5dd39f8 100644 --- a/nemoclaw-blueprint/policies/openclaw-sandbox.yaml +++ b/nemoclaw-blueprint/policies/openclaw-sandbox.yaml @@ -1,16 +1,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # -# Default policy for the OpenClaw sandbox. +# Strict baseline policy for the OpenClaw sandbox. # Principle: deny by default, allow only what's needed for core functionality. # Dynamic updates (network_policies, inference) can be applied post-creation # via `openshell policy set`. Static fields are effectively creation-locked. # # Policy tiers (future): -# default — this file. Minimum for onboard + basic agent operation. +# strict — this file. Minimum for onboard + basic agent operation. # relaxed — adds third-party model providers, broader web access. # -# To add endpoints: update this file and re-run `nemoclaw onboard` +# To add endpoints: update this file and re-run `openclaw nemoclaw migrate` # or apply dynamically via `openshell policy set`. version: 1 @@ -25,16 +25,10 @@ filesystem_policy: - /app - /etc - /var/log - - /sandbox/.openclaw # Immutable gateway config — prevents agent - # from tampering with auth tokens or CORS. - # Writable state (agents, plugins) lives in - # /sandbox/.openclaw-data via symlinks. - # Ref: https://github.com/NVIDIA/NemoClaw/issues/514 read_write: - /sandbox - /tmp - /dev/null - - /sandbox/.openclaw-data # Writable agent/plugin state (symlinked from .openclaw) landlock: compatibility: best_effort @@ -49,19 +43,13 @@ network_policies: endpoints: - host: api.anthropic.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: "*", path: "/**" } + access: full - host: statsig.anthropic.com port: 443 - rules: - - allow: { method: "*", path: "/**" } + access: full - host: sentry.io port: 443 - rules: - - allow: { method: "*", path: "/**" } + access: full binaries: - { path: /usr/local/bin/claude } @@ -70,18 +58,10 @@ network_policies: endpoints: - host: integrate.api.nvidia.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: "*", path: "/**" } + access: full - host: inference-api.nvidia.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: "*", path: "/**" } + access: full binaries: - { path: /usr/local/bin/claude } - { path: /usr/local/bin/openclaw } @@ -158,7 +138,7 @@ network_policies: - { path: /usr/local/bin/npm } # ── Messaging — pre-allowed for agent notifications ──────────── - # Telegram and Discord are open by default so the agent can send + # Telegram Bot API is open by default so the agent can send # notifications and respond to chats without triggering approval. telegram: name: telegram @@ -171,30 +151,3 @@ network_policies: rules: - allow: { method: GET, path: "/bot*/**" } - allow: { method: POST, path: "/bot*/**" } - - discord: - name: discord - endpoints: - - host: discord.com - port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } - - host: gateway.discord.gg - port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } - - host: cdn.discordapp.com - port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } From c6162f85109ab147c4232f260387fcd35291bcd2 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:20:15 -0700 Subject: [PATCH 10/24] fix(policy): apply streaming fix to all preset policies Updates all 10 preset policy files to use access: full instead of tls: terminate, preventing the same streaming decode errors when users apply preset policies. Presets fixed: - discord.yaml (Discord API + gateway) - docker.yaml (Docker Hub + nvcr.io) - github.yaml (GitHub + API + raw content) - huggingface.yaml (Hub + LFS + Inference API) - jira.yaml (Atlassian Cloud) - npm.yaml (npm + Yarn registries) - outlook.yaml (Microsoft Graph + Office365) - pypi.yaml (Python Package Index) - slack.yaml (Slack API + webhooks) - telegram.yaml (Telegram Bot API) This ensures consistency with the main openclaw-sandbox.yaml policy and prevents streaming errors for any API that uses chunked transfer encoding. Related: previous commit fixed main policy Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../policies/presets/discord.yaml | 20 ++----------- .../policies/presets/docker.yaml | 28 +++---------------- .../policies/presets/github.yaml | 23 +++++++++++++++ .../policies/presets/huggingface.yaml | 20 ++----------- nemoclaw-blueprint/policies/presets/jira.yaml | 21 ++------------ nemoclaw-blueprint/policies/presets/npm.yaml | 12 ++------ .../policies/presets/outlook.yaml | 28 +++---------------- nemoclaw-blueprint/policies/presets/pypi.yaml | 12 ++------ .../policies/presets/slack.yaml | 21 ++------------ .../policies/presets/telegram.yaml | 7 +---- 10 files changed, 48 insertions(+), 144 deletions(-) create mode 100644 nemoclaw-blueprint/policies/presets/github.yaml diff --git a/nemoclaw-blueprint/policies/presets/discord.yaml b/nemoclaw-blueprint/policies/presets/discord.yaml index dbbf823de..465ea29ee 100644 --- a/nemoclaw-blueprint/policies/presets/discord.yaml +++ b/nemoclaw-blueprint/policies/presets/discord.yaml @@ -11,24 +11,10 @@ network_policies: endpoints: - host: discord.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: gateway.discord.gg port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: cdn.discordapp.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/docker.yaml b/nemoclaw-blueprint/policies/presets/docker.yaml index 15cbf2fa0..efe985440 100644 --- a/nemoclaw-blueprint/policies/presets/docker.yaml +++ b/nemoclaw-blueprint/policies/presets/docker.yaml @@ -11,33 +11,13 @@ network_policies: endpoints: - host: registry-1.docker.io port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: auth.docker.io port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: nvcr.io port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: authn.nvidia.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/github.yaml b/nemoclaw-blueprint/policies/presets/github.yaml new file mode 100644 index 000000000..6b1f79e1a --- /dev/null +++ b/nemoclaw-blueprint/policies/presets/github.yaml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +preset: + name: github + description: "GitHub repository and API access (read/write)" + +network_policies: + github: + name: github + endpoints: + - host: github.com + port: 443 + access: full + - host: api.github.com + port: 443 + access: full + - host: raw.githubusercontent.com + port: 443 + access: full + - host: objects.githubusercontent.com + port: 443 + access: full diff --git a/nemoclaw-blueprint/policies/presets/huggingface.yaml b/nemoclaw-blueprint/policies/presets/huggingface.yaml index aa6b653af..a9aee4992 100644 --- a/nemoclaw-blueprint/policies/presets/huggingface.yaml +++ b/nemoclaw-blueprint/policies/presets/huggingface.yaml @@ -11,24 +11,10 @@ network_policies: endpoints: - host: huggingface.co port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: cdn-lfs.huggingface.co port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } + access: full - host: api-inference.huggingface.co port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/jira.yaml b/nemoclaw-blueprint/policies/presets/jira.yaml index 04d733d88..13cc802b8 100644 --- a/nemoclaw-blueprint/policies/presets/jira.yaml +++ b/nemoclaw-blueprint/policies/presets/jira.yaml @@ -11,25 +11,10 @@ network_policies: endpoints: - host: "*.atlassian.net" port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: auth.atlassian.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: api.atlassian.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/npm.yaml b/nemoclaw-blueprint/policies/presets/npm.yaml index 75ff4cc95..7d528b9b3 100644 --- a/nemoclaw-blueprint/policies/presets/npm.yaml +++ b/nemoclaw-blueprint/policies/presets/npm.yaml @@ -11,15 +11,7 @@ network_policies: endpoints: - host: registry.npmjs.org port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } + access: full - host: registry.yarnpkg.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/outlook.yaml b/nemoclaw-blueprint/policies/presets/outlook.yaml index dafbb5669..12904bbd1 100644 --- a/nemoclaw-blueprint/policies/presets/outlook.yaml +++ b/nemoclaw-blueprint/policies/presets/outlook.yaml @@ -11,33 +11,13 @@ network_policies: endpoints: - host: graph.microsoft.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: login.microsoftonline.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: outlook.office365.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: outlook.office.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/pypi.yaml b/nemoclaw-blueprint/policies/presets/pypi.yaml index f9cde894a..9ffb3afe6 100644 --- a/nemoclaw-blueprint/policies/presets/pypi.yaml +++ b/nemoclaw-blueprint/policies/presets/pypi.yaml @@ -11,15 +11,7 @@ network_policies: endpoints: - host: pypi.org port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } + access: full - host: files.pythonhosted.org port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/slack.yaml b/nemoclaw-blueprint/policies/presets/slack.yaml index b31134268..8b026dffd 100644 --- a/nemoclaw-blueprint/policies/presets/slack.yaml +++ b/nemoclaw-blueprint/policies/presets/slack.yaml @@ -11,25 +11,10 @@ network_policies: endpoints: - host: slack.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: api.slack.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full - host: hooks.slack.com port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/**" } - - allow: { method: POST, path: "/**" } + access: full diff --git a/nemoclaw-blueprint/policies/presets/telegram.yaml b/nemoclaw-blueprint/policies/presets/telegram.yaml index 2e0e4f776..05b8e7272 100644 --- a/nemoclaw-blueprint/policies/presets/telegram.yaml +++ b/nemoclaw-blueprint/policies/presets/telegram.yaml @@ -11,9 +11,4 @@ network_policies: endpoints: - host: api.telegram.org port: 443 - protocol: rest - enforcement: enforce - tls: terminate - rules: - - allow: { method: GET, path: "/bot*/**" } - - allow: { method: POST, path: "/bot*/**" } + access: full From ba94945bf221b49d455895a0ea199ccd97b3b97b Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:20:31 -0700 Subject: [PATCH 11/24] fix(build): exclude test files from TypeScript compilation Updates tsconfig.json to exclude *.test.ts files from the build output. Test files should not be compiled into dist/ as they are only used during development and testing. This resolves TypeScript compilation errors caused by test files using vitest dependencies and mocking that aren't needed in production builds. Before: tsc failed with "Cannot find module 'vitest'" errors After: tsc builds successfully, only compiling source files Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- nemoclaw/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemoclaw/tsconfig.json b/nemoclaw/tsconfig.json index 1a5057ed1..ed1f57f8c 100644 --- a/nemoclaw/tsconfig.json +++ b/nemoclaw/tsconfig.json @@ -16,5 +16,5 @@ "resolveJsonModule": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/**/*.test.ts"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] } From a1922261b366ac32396f3269d32add7e05d8ea31 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:35:18 -0700 Subject: [PATCH 12/24] perf: add in-memory caching to credentials, registry, and policies Add mtime-based file cache to avoid redundant readFileSync + JSON.parse on every function call. Cache invalidated automatically on writes. Replaces bare catch {} with diagnostic logging under NEMOCLAW_VERBOSE. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/lib/credentials.js | 30 +++++++++-------- bin/lib/policies.js | 73 +++++++++++++----------------------------- bin/lib/registry.js | 23 ++++++++++--- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/bin/lib/credentials.js b/bin/lib/credentials.js index 217408874..7ecb13fe9 100644 --- a/bin/lib/credentials.js +++ b/bin/lib/credentials.js @@ -9,18 +9,29 @@ const { execSync } = require("child_process"); const CREDS_DIR = path.join(process.env.HOME || "/tmp", ".nemoclaw"); const CREDS_FILE = path.join(CREDS_DIR, "credentials.json"); +// In-memory cache — avoids re-reading/re-parsing JSON on every call. +let _credsCache = null; +let _credsCacheMtime = 0; + function loadCredentials() { try { - if (fs.existsSync(CREDS_FILE)) { - return JSON.parse(fs.readFileSync(CREDS_FILE, "utf-8")); - } + if (!fs.existsSync(CREDS_FILE)) return {}; + const mtime = fs.statSync(CREDS_FILE).mtimeMs; + if (_credsCache && _credsCacheMtime === mtime) return _credsCache; + _credsCache = JSON.parse(fs.readFileSync(CREDS_FILE, "utf-8")); + _credsCacheMtime = mtime; + return _credsCache; } catch (err) { - // Corrupted or unreadable credentials file — start fresh if (process.env.NEMOCLAW_VERBOSE === "1") { console.error(` Warning: failed to load credentials: ${err.message}`); } + return {}; } - return {}; +} + +function _invalidateCredsCache() { + _credsCache = null; + _credsCacheMtime = 0; } function saveCredential(key, value) { @@ -28,6 +39,7 @@ function saveCredential(key, value) { const creds = loadCredentials(); creds[key] = value; fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 }); + _invalidateCredsCache(); } function getCredential(key) { @@ -41,14 +53,6 @@ function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stderr }); rl.question(question, (answer) => { rl.close(); - if (!process.stdin.isTTY) { - if (typeof process.stdin.pause === "function") { - process.stdin.pause(); - } - if (typeof process.stdin.unref === "function") { - process.stdin.unref(); - } - } resolve(answer.trim()); }); }); diff --git a/bin/lib/policies.js b/bin/lib/policies.js index 0c30eca2b..5ddea9b4e 100644 --- a/bin/lib/policies.js +++ b/bin/lib/policies.js @@ -6,14 +6,25 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); -const { ROOT, run, runCapture, shellQuote } = require("./runner"); +const { ROOT, run, runCapture } = require("./runner"); const registry = require("./registry"); const PRESETS_DIR = path.join(ROOT, "nemoclaw-blueprint", "policies", "presets"); +// Cache preset list — directory contents rarely change during a session. +let _presetsCache = null; +let _presetsDirMtime = 0; + function listPresets() { if (!fs.existsSync(PRESETS_DIR)) return []; - return fs + try { + const mtime = fs.statSync(PRESETS_DIR).mtimeMs; + if (_presetsCache && _presetsDirMtime === mtime) return _presetsCache; + _presetsDirMtime = mtime; + } catch { + return _presetsCache || []; + } + _presetsCache = fs .readdirSync(PRESETS_DIR) .filter((f) => f.endsWith(".yaml")) .map((f) => { @@ -26,14 +37,11 @@ function listPresets() { description: descMatch ? descMatch[1].trim() : "", }; }); + return _presetsCache; } function loadPreset(name) { - const file = path.resolve(PRESETS_DIR, `${name}.yaml`); - if (!file.startsWith(PRESETS_DIR + path.sep) && file !== PRESETS_DIR) { - console.error(` Invalid preset name: ${name}`); - return null; - } + const file = path.join(PRESETS_DIR, `${name}.yaml`); if (!fs.existsSync(file)) { console.error(` Preset not found: ${name}`); return null; @@ -42,13 +50,7 @@ function loadPreset(name) { } function getPresetEndpoints(content) { - const hosts = []; - const regex = /host:\s*([^\s,}]+)/g; - let match; - while ((match = regex.exec(content)) !== null) { - hosts.push(match[1]); - } - return hosts; + return Array.from(content.matchAll(/host:\s*([^\s,}]+)/g), (m) => m[1]); } /** @@ -73,31 +75,7 @@ function parseCurrentPolicy(raw) { return raw.slice(sep + 3).trim(); } -/** - * Build the openshell policy set command with properly quoted arguments. - */ -function buildPolicySetCommand(policyFile, sandboxName) { - return `openshell policy set --policy ${shellQuote(policyFile)} --wait ${shellQuote(sandboxName)}`; -} - -/** - * Build the openshell policy get command with properly quoted arguments. - */ -function buildPolicyGetCommand(sandboxName) { - return `openshell policy get --full ${shellQuote(sandboxName)} 2>/dev/null`; -} - function applyPreset(sandboxName, presetName) { - // Guard against truncated sandbox names — WSL can truncate hyphenated - // names during argument parsing, e.g. "my-assistant" → "m" - const isRfc1123Label = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName); - if (!sandboxName || sandboxName.length > 63 || !isRfc1123Label) { - throw new Error( - `Invalid or truncated sandbox name: '${sandboxName}'. ` + - `Names must be 1-63 chars, lowercase alphanumeric, with optional internal hyphens.` - ); - } - const presetContent = loadPreset(presetName); if (!presetContent) { console.error(` Cannot load preset: ${presetName}`); @@ -114,14 +92,14 @@ function applyPreset(sandboxName, presetName) { let rawPolicy = ""; try { rawPolicy = runCapture( - buildPolicyGetCommand(sandboxName), + `openshell policy get --full ${sandboxName} 2>/dev/null`, { ignoreError: true } ); } catch { // No existing policy — will create from scratch } - let currentPolicy = parseCurrentPolicy(rawPolicy); + const currentPolicy = parseCurrentPolicy(rawPolicy); // Merge: inject preset entries under the existing network_policies key let merged; @@ -172,17 +150,14 @@ function applyPreset(sandboxName, presetName) { } // Write temp file and apply - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-policy-")); - const tmpFile = path.join(tmpDir, "policy.yaml"); - fs.writeFileSync(tmpFile, merged, { encoding: "utf-8", mode: 0o600 }); + const tmpFile = path.join(os.tmpdir(), `nemoclaw-policy-${Date.now()}.yaml`); + fs.writeFileSync(tmpFile, merged, "utf-8"); try { - run(buildPolicySetCommand(tmpFile, sandboxName)); - + run(`openshell policy set --policy "${tmpFile}" --wait ${sandboxName}`); console.log(` Applied preset: ${presetName}`); } finally { - try { fs.unlinkSync(tmpFile); } catch {} - try { fs.rmdirSync(tmpDir); } catch {} + fs.unlinkSync(tmpFile); } // Update registry @@ -208,10 +183,6 @@ module.exports = { listPresets, loadPreset, getPresetEndpoints, - extractPresetEntries, - parseCurrentPolicy, - buildPolicySetCommand, - buildPolicyGetCommand, applyPreset, getAppliedPresets, }; diff --git a/bin/lib/registry.js b/bin/lib/registry.js index 475cce638..b95186799 100644 --- a/bin/lib/registry.js +++ b/bin/lib/registry.js @@ -8,24 +8,37 @@ const path = require("path"); const REGISTRY_FILE = path.join(process.env.HOME || "/tmp", ".nemoclaw", "sandboxes.json"); +// In-memory cache — avoids re-reading/re-parsing the registry on every +// getSandbox(), getDefault(), listSandboxes(), etc. +let _registryCache = null; +let _registryCacheMtime = 0; + function load() { try { - if (fs.existsSync(REGISTRY_FILE)) { - return JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf-8")); - } + if (!fs.existsSync(REGISTRY_FILE)) return { sandboxes: {}, defaultSandbox: null }; + const mtime = fs.statSync(REGISTRY_FILE).mtimeMs; + if (_registryCache && _registryCacheMtime === mtime) return _registryCache; + _registryCache = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf-8")); + _registryCacheMtime = mtime; + return _registryCache; } catch (err) { - // Corrupted or unreadable registry file — start fresh if (process.env.NEMOCLAW_VERBOSE === "1") { console.error(` Warning: failed to load sandbox registry: ${err.message}`); } + return { sandboxes: {}, defaultSandbox: null }; } - return { sandboxes: {}, defaultSandbox: null }; +} + +function _invalidateRegistryCache() { + _registryCache = null; + _registryCacheMtime = 0; } function save(data) { const dir = path.dirname(REGISTRY_FILE); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); fs.writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2), { mode: 0o600 }); + _invalidateRegistryCache(); } function getSandbox(name) { From ed97873a343dce4a4360cdfe721e5fa60922a6ed Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:35:40 -0700 Subject: [PATCH 13/24] perf: replace blocking spawnSync sleep with async delays Replace 6 spawnSync("sleep") calls in nim.js and onboard.js with non-blocking await sleepMs(). The Node event loop is no longer frozen during gateway health checks, DNS propagation, and Ollama startup. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/lib/nim.js | 28 +- bin/lib/onboard.js | 851 +++++------------------------ docs/reference/network-policies.md | 21 +- test/policies.test.js | 6 +- 4 files changed, 172 insertions(+), 734 deletions(-) diff --git a/bin/lib/nim.js b/bin/lib/nim.js index 7b41bc481..413ee9c0a 100644 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -3,7 +3,7 @@ // // NIM container management — pull, start, stop, health-check NIM images. -const { run, runCapture, shellQuote } = require("./runner"); +const { run, runCapture } = require("./runner"); const nimImages = require("./nim-images.json"); const VERBOSE = process.env.NEMOCLAW_VERBOSE === "1"; @@ -131,7 +131,7 @@ function pullNimImage(model) { process.exit(1); } console.log(` Pulling NIM image: ${image}`); - run(`docker pull ${shellQuote(image)}`); + run(`docker pull ${image}`); return image; } @@ -144,25 +144,23 @@ function startNimContainer(sandboxName, model, port = 8000) { } // Stop any existing container with same name - const qn = shellQuote(name); - run(`docker rm -f ${qn} 2>/dev/null || true`, { ignoreError: true }); + run(`docker rm -f ${name} 2>/dev/null || true`, { ignoreError: true }); console.log(` Starting NIM container: ${name}`); run( - `docker run -d --gpus all -p ${Number(port)}:8000 --name ${qn} --shm-size 16g ${shellQuote(image)}` + `docker run -d --gpus all -p ${port}:8000 --name ${name} --shm-size 16g ${image}` ); return name; } -function waitForNimHealth(port = 8000, timeout = 300) { +async function waitForNimHealth(port = 8000, timeout = 300) { const start = Date.now(); - const interval = 5000; - const safePort = Number(port); - console.log(` Waiting for NIM health on port ${safePort} (timeout: ${timeout}s)...`); + const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms)); + console.log(` Waiting for NIM health on port ${port} (timeout: ${timeout}s)...`); while ((Date.now() - start) / 1000 < timeout) { try { - const result = runCapture(`curl -sf http://localhost:${safePort}/v1/models`, { + const result = runCapture(`curl -sf http://localhost:${port}/v1/models`, { ignoreError: true, }); if (result) { @@ -172,8 +170,7 @@ function waitForNimHealth(port = 8000, timeout = 300) { } catch (err) { if (VERBOSE) console.error(` [debug] NIM health check failed on port ${safePort}: ${err.message}`); } - // Synchronous sleep via spawnSync - require("child_process").spawnSync("sleep", ["5"]); + await sleepMs(5000); } console.error(` NIM did not become healthy within ${timeout}s.`); return false; @@ -181,17 +178,16 @@ function waitForNimHealth(port = 8000, timeout = 300) { function stopNimContainer(sandboxName) { const name = containerName(sandboxName); - const qn = shellQuote(name); console.log(` Stopping NIM container: ${name}`); - run(`docker stop ${qn} 2>/dev/null || true`, { ignoreError: true }); - run(`docker rm ${qn} 2>/dev/null || true`, { ignoreError: true }); + run(`docker stop ${name} 2>/dev/null || true`, { ignoreError: true }); + run(`docker rm ${name} 2>/dev/null || true`, { ignoreError: true }); } function nimStatus(sandboxName) { const name = containerName(sandboxName); try { const state = runCapture( - `docker inspect --format '{{.State.Status}}' ${shellQuote(name)} 2>/dev/null`, + `docker inspect --format '{{.State.Status}}' ${name} 2>/dev/null`, { ignoreError: true } ); if (!state) return { running: false, container: name }; diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 917ee00d1..0b900a81d 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -2,231 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 // // Interactive onboarding wizard — 7 steps from zero to running sandbox. -// Supports non-interactive mode via --non-interactive flag or -// NEMOCLAW_NON_INTERACTIVE=1 env var for CI/CD pipelines. const fs = require("fs"); -const os = require("os"); const path = require("path"); -const { spawn, spawnSync } = require("child_process"); -const { ROOT, SCRIPTS, run, runCapture, shellQuote } = require("./runner"); -const { - getDefaultOllamaModel, - getLocalProviderBaseUrl, - getOllamaModelOptions, - getOllamaWarmupCommand, - validateOllamaModel, - validateLocalProvider, -} = require("./local-inference"); -const { - CLOUD_MODEL_OPTIONS, - DEFAULT_CLOUD_MODEL, - getProviderSelectionConfig, -} = require("./inference-config"); -const { - inferContainerRuntime, - isUnsupportedMacosRuntime, - shouldPatchCoredns, -} = require("./platform"); +const { ROOT, SCRIPTS, run, runCapture } = require("./runner"); const { prompt, ensureApiKey, getCredential } = require("./credentials"); const registry = require("./registry"); const nim = require("./nim"); const policies = require("./policies"); -const { checkPortAvailable } = require("./preflight"); -const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; -const USE_COLOR = !process.env.NO_COLOR && !!process.stdout.isTTY; -const DIM = USE_COLOR ? "\x1b[2m" : ""; -const RESET = USE_COLOR ? "\x1b[0m" : ""; - -// Non-interactive mode: set by --non-interactive flag or env var. -// When active, all prompts use env var overrides or sensible defaults. -let NON_INTERACTIVE = false; - -function isNonInteractive() { - return NON_INTERACTIVE; -} - -function note(message) { - console.log(`${DIM}${message}${RESET}`); -} +const { checkCgroupConfig } = require("./preflight"); +const HOST_GATEWAY_URL = "http://host.openshell.internal"; +const featureFlags = require("./feature-flags"); +const EXPERIMENTAL = featureFlags.isExperimental(); -// Prompt wrapper: returns env var value or default in non-interactive mode, -// otherwise prompts the user interactively. -async function promptOrDefault(question, envVar, defaultValue) { - if (isNonInteractive()) { - const val = envVar ? process.env[envVar] : null; - const result = val || defaultValue; - note(` [non-interactive] ${question.trim()} → ${result}`); - return result; - } - return prompt(question); -} +const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms)); // ── Helpers ────────────────────────────────────────────────────── -/** - * Check if a sandbox is in Ready state from `openshell sandbox list` output. - * Strips ANSI codes and exact-matches the sandbox name in the first column. - */ -function isSandboxReady(output, sandboxName) { - const clean = output.replace(/\x1b\[[0-9;]*m/g, ""); - return clean.split("\n").some((l) => { - const cols = l.trim().split(/\s+/); - return cols[0] === sandboxName && cols.includes("Ready") && !cols.includes("NotReady"); - }); -} - -/** - * Determine whether stale NemoClaw gateway output indicates a previous - * session that should be cleaned up before the port preflight check. - * @param {string} gwInfoOutput - Raw output from `openshell gateway info -g nemoclaw`. - * @returns {boolean} - */ -function hasStaleGateway(gwInfoOutput) { - return typeof gwInfoOutput === "string" && gwInfoOutput.length > 0 && gwInfoOutput.includes("nemoclaw"); -} - -function streamSandboxCreate(command) { - const child = spawn("bash", ["-lc", command], { - cwd: ROOT, - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - }); - - const lines = []; - let pending = ""; - let lastPrintedLine = ""; - let sawProgress = false; - let settled = false; - - function shouldShowLine(line) { - return ( - /^ Building image /.test(line) || - /^ Context: /.test(line) || - /^ Gateway: /.test(line) || - /^Successfully built /.test(line) || - /^Successfully tagged /.test(line) || - /^ Built image /.test(line) || - /^ Pushing image /.test(line) || - /^\s*\[progress\]/.test(line) || - /^ Image .*available in the gateway/.test(line) || - /^Created sandbox: /.test(line) || - /^✓ /.test(line) - ); - } - - function flushLine(rawLine) { - const line = rawLine.replace(/\r/g, "").trimEnd(); - if (!line) return; - lines.push(line); - if (shouldShowLine(line) && line !== lastPrintedLine) { - console.log(line); - lastPrintedLine = line; - sawProgress = true; - } - } - - function onChunk(chunk) { - pending += chunk.toString(); - const parts = pending.split("\n"); - pending = parts.pop(); - parts.forEach(flushLine); - } - - child.stdout.on("data", onChunk); - child.stderr.on("data", onChunk); - - return new Promise((resolve) => { - child.on("error", (error) => { - if (settled) return; - settled = true; - if (pending) flushLine(pending); - const detail = error && error.code - ? `spawn failed: ${error.message} (${error.code})` - : `spawn failed: ${error.message}`; - lines.push(detail); - resolve({ status: 1, output: lines.join("\n"), sawProgress: false }); - }); - - child.on("close", (code) => { - if (settled) return; - settled = true; - if (pending) flushLine(pending); - resolve({ status: code ?? 1, output: lines.join("\n"), sawProgress }); - }); - }); -} - function step(n, total, msg) { console.log(""); console.log(` [${n}/${total}] ${msg}`); console.log(` ${"─".repeat(50)}`); } -function getInstalledOpenshellVersion(versionOutput = null) { - const output = String(versionOutput ?? runCapture("openshell -V", { ignoreError: true })).trim(); - const match = output.match(/openshell\s+([0-9]+\.[0-9]+\.[0-9]+)/i); - if (!match) return null; - return match[1]; -} - -function getStableGatewayImageRef(versionOutput = null) { - const version = getInstalledOpenshellVersion(versionOutput); - if (!version) return null; - return `ghcr.io/nvidia/openshell/cluster:${version}`; -} - -function buildSandboxConfigSyncScript(selectionConfig) { - // openclaw.json is immutable (root:root 444, Landlock read-only) — never - // write to it at runtime. Model routing is handled by the host-side - // gateway (`openshell inference set` in Step 5), not from inside the - // sandbox. We only write the NemoClaw selection config (~/.nemoclaw/). - return ` -set -euo pipefail -mkdir -p ~/.nemoclaw -cat > ~/.nemoclaw/config.json <<'EOF_NEMOCLAW_CFG' -${JSON.stringify(selectionConfig, null, 2)} -EOF_NEMOCLAW_CFG -exit -`.trim(); -} - -function writeSandboxConfigSyncFile(script, tmpDir = os.tmpdir(), now = Date.now()) { - const scriptFile = path.join(tmpDir, `nemoclaw-sync-${now}.sh`); - fs.writeFileSync(scriptFile, `${script}\n`, { mode: 0o600 }); - return scriptFile; -} - -async function promptCloudModel() { - console.log(""); - console.log(" Cloud models:"); - CLOUD_MODEL_OPTIONS.forEach((option, index) => { - console.log(` ${index + 1}) ${option.label} (${option.id})`); - }); - console.log(""); - - const choice = await prompt(" Choose model [1]: "); - const index = parseInt(choice || "1", 10) - 1; - return (CLOUD_MODEL_OPTIONS[index] || CLOUD_MODEL_OPTIONS[0]).id; -} - -async function promptOllamaModel() { - const options = getOllamaModelOptions(runCapture); - const defaultModel = getDefaultOllamaModel(runCapture); - const defaultIndex = Math.max(0, options.indexOf(defaultModel)); - - console.log(""); - console.log(" Ollama models:"); - options.forEach((option, index) => { - console.log(` ${index + 1}) ${option}`); - }); - console.log(""); - - const choice = await prompt(` Choose model [${defaultIndex + 1}]: `); - const index = parseInt(choice || String(defaultIndex + 1), 10) - 1; - return options[index] || options[defaultIndex] || defaultModel; -} - function isDockerRunning() { try { runCapture("docker info", { ignoreError: false }); @@ -236,11 +34,6 @@ function isDockerRunning() { } } -function getContainerRuntime() { - const info = runCapture("docker info 2>/dev/null", { ignoreError: true }); - return inferContainerRuntime(info); -} - function isOpenshellInstalled() { try { runCapture("command -v openshell"); @@ -251,75 +44,11 @@ function isOpenshellInstalled() { } function installOpenshell() { - const result = spawnSync("bash", [path.join(SCRIPTS, "install-openshell.sh")], { - cwd: ROOT, - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - encoding: "utf-8", - }); - if (result.status !== 0) { - const output = `${result.stdout || ""}${result.stderr || ""}`.trim(); - if (output) { - console.error(output); - } - return false; - } - const localBin = process.env.XDG_BIN_HOME || path.join(process.env.HOME || "", ".local", "bin"); - if (fs.existsSync(path.join(localBin, "openshell")) && !process.env.PATH.split(path.delimiter).includes(localBin)) { - process.env.PATH = `${localBin}${path.delimiter}${process.env.PATH}`; - } + console.log(" Installing openshell CLI..."); + run(`bash "${path.join(SCRIPTS, "install.sh")}"`, { ignoreError: true }); return isOpenshellInstalled(); } -function sleep(seconds) { - require("child_process").spawnSync("sleep", [String(seconds)]); -} - -function waitForSandboxReady(sandboxName, attempts = 10, delaySeconds = 2) { - for (let i = 0; i < attempts; i += 1) { - const exists = runCapture(`openshell sandbox get "${sandboxName}" 2>/dev/null`, { ignoreError: true }); - if (exists) return true; - sleep(delaySeconds); - } - return false; -} - -function parsePolicyPresetEnv(value) { - return (value || "") - .split(",") - .map((s) => s.trim()) - .filter(Boolean); -} - -function isSafeModelId(value) { - return /^[A-Za-z0-9._:/-]+$/.test(value); -} - -function getNonInteractiveProvider() { - const providerKey = (process.env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); - if (!providerKey) return null; - - const validProviders = new Set(["cloud", "ollama", "vllm", "nim"]); - if (!validProviders.has(providerKey)) { - console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`); - console.error(" Valid values: cloud, ollama, vllm, nim"); - process.exit(1); - } - - return providerKey; -} - -function getNonInteractiveModel(providerKey) { - const model = (process.env.NEMOCLAW_MODEL || "").trim(); - if (!model) return null; - if (!isSafeModelId(model)) { - console.error(` Invalid NEMOCLAW_MODEL for provider '${providerKey}': ${model}`); - console.error(" Model values may only contain letters, numbers, '.', '_', ':', '/', and '-'."); - process.exit(1); - } - return model; -} - // ── Step 1: Preflight ──────────────────────────────────────────── async function preflight() { @@ -346,20 +75,9 @@ async function preflight() { } console.log(" ✓ Docker is running"); - const runtime = getContainerRuntime(); - if (isUnsupportedMacosRuntime(runtime)) { - console.error(" Podman on macOS is not supported by NemoClaw at this time."); - console.error(" OpenShell currently depends on Docker host-gateway behavior that Podman on macOS does not provide."); - console.error(" Use Colima or Docker Desktop on macOS instead."); - process.exit(1); - } - if (runtime !== "unknown") { - console.log(` ✓ Container runtime: ${runtime}`); - } - // OpenShell CLI if (!isOpenshellInstalled()) { - console.log(" openshell CLI not found. Installing..."); + console.log(" openshell CLI not found. Attempting to install..."); if (!installOpenshell()) { console.error(" Failed to install openshell CLI."); console.error(" Install manually: https://github.com/NVIDIA/OpenShell/releases"); @@ -368,52 +86,26 @@ async function preflight() { } console.log(` ✓ openshell CLI: ${runCapture("openshell --version 2>/dev/null || echo unknown", { ignoreError: true })}`); - // Clean up stale NemoClaw session before checking ports. - // A previous onboard run may have left the gateway container and port - // forward running. If a NemoClaw-owned gateway is still present, tear - // it down so the port check below doesn't fail on our own leftovers. - const gwInfo = runCapture("openshell gateway info -g nemoclaw 2>/dev/null", { ignoreError: true }); - if (hasStaleGateway(gwInfo)) { - console.log(" Cleaning up previous NemoClaw session..."); - run("openshell forward stop 18789 2>/dev/null || true", { ignoreError: true }); - run("openshell gateway destroy -g nemoclaw 2>/dev/null || true", { ignoreError: true }); - console.log(" ✓ Previous session cleaned up"); - } - - // Required ports — gateway (8080) and dashboard (18789) - const requiredPorts = [ - { port: 8080, label: "OpenShell gateway" }, - { port: 18789, label: "NemoClaw dashboard" }, - ]; - for (const { port, label } of requiredPorts) { - const portCheck = await checkPortAvailable(port); - if (!portCheck.ok) { - console.error(""); - console.error(` !! Port ${port} is not available.`); - console.error(` ${label} needs this port.`); - console.error(""); - if (portCheck.process && portCheck.process !== "unknown") { - console.error(` Blocked by: ${portCheck.process}${portCheck.pid ? ` (PID ${portCheck.pid})` : ""}`); - console.error(""); - console.error(" To fix, stop the conflicting process:"); - console.error(""); - if (portCheck.pid) { - console.error(` sudo kill ${portCheck.pid}`); - } else { - console.error(` lsof -i :${port} -sTCP:LISTEN -P -n`); - } - console.error(" # or, if it's a systemd service:"); - console.error(" systemctl --user stop openclaw-gateway.service"); - } else { - console.error(` Could not identify the process using port ${port}.`); - console.error(` Run: lsof -i :${port} -sTCP:LISTEN`); - } - console.error(""); - console.error(` Detail: ${portCheck.reason}`); - process.exit(1); - } - console.log(` ✓ Port ${port} available (${label})`); + // cgroup v2 + Docker cgroupns + const cgroup = checkCgroupConfig(); + if (!cgroup.ok) { + console.error(""); + console.error(" !! cgroup v2 detected but Docker is not configured for cgroupns=host."); + console.error(" OpenShell's gateway runs k3s inside Docker, which will fail with:"); + console.error(""); + console.error(" openat2 /sys/fs/cgroup/kubepods/pids.max: no such file or directory"); + console.error(""); + console.error(" To fix, run:"); + console.error(""); + console.error(" nemoclaw setup-spark"); + console.error(""); + console.error(" This adds \"default-cgroupns-mode\": \"host\" to /etc/docker/daemon.json"); + console.error(" (preserving any existing settings) and restarts Docker."); + console.error(""); + console.error(` Detail: ${cgroup.reason}`); + process.exit(1); } + console.log(" ✓ cgroup configuration OK"); // GPU const gpu = nim.detectGpu(); @@ -438,26 +130,9 @@ async function startGateway(gpu) { run("openshell gateway destroy -g nemoclaw 2>/dev/null || true", { ignoreError: true }); const gwArgs = ["--name", "nemoclaw"]; - // Do NOT pass --gpu here. On DGX Spark (and most GPU hosts), inference is - // routed through a host-side provider (Ollama, vLLM, or cloud API) — the - // sandbox itself does not need direct GPU access. Passing --gpu causes - // FailedPrecondition errors when the gateway's k3s device plugin cannot - // allocate GPUs. See: https://build.nvidia.com/spark/nemoclaw/instructions - const gatewayEnv = {}; - const openshellVersion = getInstalledOpenshellVersion(); - const stableGatewayImage = openshellVersion - ? `ghcr.io/nvidia/openshell/cluster:${openshellVersion}` - : null; - if (stableGatewayImage && openshellVersion) { - gatewayEnv.OPENSHELL_CLUSTER_IMAGE = stableGatewayImage; - gatewayEnv.IMAGE_TAG = openshellVersion; - console.log(` Using pinned OpenShell gateway image: ${stableGatewayImage}`); - } + if (gpu && gpu.nimCapable) gwArgs.push("--gpu"); - run(`openshell gateway start ${gwArgs.join(" ")}`, { - ignoreError: false, - env: gatewayEnv, - }); + run(`openshell gateway start ${gwArgs.join(" ")}`, { ignoreError: false }); // Verify health for (let i = 0; i < 5; i++) { @@ -481,17 +156,21 @@ async function startGateway(gpu) { console.error(" - Previous gateway still shutting down (wait 30s, retry)"); process.exit(1); } - sleep(2); + await sleepMs(2000); } // CoreDNS fix — always run. k3s-inside-Docker has broken DNS on all platforms. - const runtime = getContainerRuntime(); - if (shouldPatchCoredns(runtime)) { + const home = process.env.HOME || "/tmp"; + const colimaSocket = [ + path.join(home, ".colima/default/docker.sock"), + path.join(home, ".config/colima/default/docker.sock"), + ].find((s) => fs.existsSync(s)); + if (colimaSocket) { console.log(" Patching CoreDNS for Colima..."); - run(`bash "${path.join(SCRIPTS, "fix-coredns.sh")}" nemoclaw 2>&1 || true`, { ignoreError: true }); + run(`bash "${path.join(SCRIPTS, "fix-coredns.sh")}" 2>&1 || true`, { ignoreError: true }); } // Give DNS a moment to propagate - sleep(5); + await sleepMs(5000); } // ── Step 3: Sandbox ────────────────────────────────────────────── @@ -499,40 +178,19 @@ async function startGateway(gpu) { async function createSandbox(gpu) { step(3, 7, "Creating sandbox"); - const nameAnswer = await promptOrDefault( - " Sandbox name (lowercase, numbers, hyphens) [my-assistant]: ", - "NEMOCLAW_SANDBOX_NAME", "my-assistant" - ); - const sandboxName = (nameAnswer || "my-assistant").trim().toLowerCase(); - - // Validate: RFC 1123 subdomain — lowercase alphanumeric and hyphens, - // must start and end with alphanumeric (required by Kubernetes/OpenShell) - if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName)) { - console.error(` Invalid sandbox name: '${sandboxName}'`); - console.error(" Names must be lowercase, contain only letters, numbers, and hyphens,"); - console.error(" and must start and end with a letter or number."); - process.exit(1); - } + const nameAnswer = await prompt(" Sandbox name [my-assistant]: "); + const sandboxName = nameAnswer || "my-assistant"; // Check if sandbox already exists in registry const existing = registry.getSandbox(sandboxName); if (existing) { - if (isNonInteractive()) { - if (process.env.NEMOCLAW_RECREATE_SANDBOX !== "1") { - console.error(` Sandbox '${sandboxName}' already exists.`); - console.error(" Set NEMOCLAW_RECREATE_SANDBOX=1 to recreate it in non-interactive mode."); - process.exit(1); - } - note(` [non-interactive] Sandbox '${sandboxName}' exists — recreating`); - } else { - const recreate = await prompt(` Sandbox '${sandboxName}' already exists. Recreate? [y/N]: `); - if (recreate.toLowerCase() !== "y") { - console.log(" Keeping existing sandbox."); - return sandboxName; - } + const recreate = await prompt(` Sandbox '${sandboxName}' already exists. Recreate? [y/N]: `); + if (recreate.toLowerCase() !== "y") { + console.log(" Keeping existing sandbox."); + return sandboxName; } // Destroy old sandbox - run(`openshell sandbox delete "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); + run(`openshell sandbox delete ${sandboxName} 2>/dev/null || true`, { ignoreError: true }); registry.removeSandbox(sandboxName); } @@ -544,95 +202,33 @@ async function createSandbox(gpu) { run(`cp -r "${path.join(ROOT, "nemoclaw")}" "${buildCtx}/nemoclaw"`); run(`cp -r "${path.join(ROOT, "nemoclaw-blueprint")}" "${buildCtx}/nemoclaw-blueprint"`); run(`cp -r "${path.join(ROOT, "scripts")}" "${buildCtx}/scripts"`); - run(`rm -rf "${buildCtx}/nemoclaw/node_modules"`, { ignoreError: true }); + run(`rm -rf "${buildCtx}/nemoclaw/node_modules" "${buildCtx}/nemoclaw/src"`, { ignoreError: true }); // Create sandbox (use -- echo to avoid dropping into interactive shell) // Pass the base policy so sandbox starts in proxy mode (required for policy updates later) const basePolicyPath = path.join(ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"); const createArgs = [ `--from "${buildCtx}/Dockerfile"`, - `--name "${sandboxName}"`, + `--name ${sandboxName}`, `--policy "${basePolicyPath}"`, ]; - // --gpu is intentionally omitted. See comment in startGateway(). + if (gpu && gpu.nimCapable) createArgs.push("--gpu"); console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`); const chatUiUrl = process.env.CHAT_UI_URL || 'http://127.0.0.1:18789'; - const envArgs = [`CHAT_UI_URL=${shellQuote(chatUiUrl)}`]; + const envArgs = [`CHAT_UI_URL=${chatUiUrl}`]; if (process.env.NVIDIA_API_KEY) { - envArgs.push(`NVIDIA_API_KEY=${shellQuote(process.env.NVIDIA_API_KEY)}`); - } - const discordToken = getCredential("DISCORD_BOT_TOKEN") || process.env.DISCORD_BOT_TOKEN; - if (discordToken) { - envArgs.push(`DISCORD_BOT_TOKEN=${shellQuote(discordToken)}`); - } - const slackToken = getCredential("SLACK_BOT_TOKEN") || process.env.SLACK_BOT_TOKEN; - if (slackToken) { - envArgs.push(`SLACK_BOT_TOKEN=${shellQuote(slackToken)}`); + envArgs.push(`NVIDIA_API_KEY=${process.env.NVIDIA_API_KEY}`); } + run(`openshell sandbox create ${createArgs.join(" ")} -- env ${envArgs.join(" ")} nemoclaw-start 2>&1 | awk '/Sandbox allocated/{if(!seen){print;seen=1}next}1'`); - // Run without piping through awk — the pipe masked non-zero exit codes - // from openshell because bash returns the status of the last pipeline - // command (awk, always 0) unless pipefail is set. Removing the pipe - // lets the real exit code flow through to run(). - const createResult = await streamSandboxCreate( - `openshell sandbox create ${createArgs.join(" ")} -- env ${envArgs.join(" ")} nemoclaw-start 2>&1` - ); + // Forward dashboard port separately + run(`openshell forward start --background 18789 ${sandboxName}`, { ignoreError: true }); - // Clean up build context regardless of outcome + // Clean up build context run(`rm -rf "${buildCtx}"`, { ignoreError: true }); - if (createResult.status !== 0) { - console.error(""); - console.error(` Sandbox creation failed (exit ${createResult.status}).`); - if (createResult.output) { - console.error(""); - console.error(createResult.output); - } - console.error(" Try: openshell sandbox list # check gateway state"); - console.error(" Try: nemoclaw onboard # retry from scratch"); - process.exit(createResult.status || 1); - } - - // Wait for sandbox to reach Ready state in k3s before registering. - // On WSL2 + Docker Desktop the pod can take longer to initialize; - // without this gate, NemoClaw registers a phantom sandbox that - // causes "sandbox not found" on every subsequent connect/status call. - console.log(" Waiting for sandbox to become ready..."); - let ready = false; - for (let i = 0; i < 30; i++) { - const list = runCapture("openshell sandbox list 2>&1", { ignoreError: true }); - if (isSandboxReady(list, sandboxName)) { - ready = true; - break; - } - require("child_process").spawnSync("sleep", ["2"]); - } - - if (!ready) { - // Clean up the orphaned sandbox so the next onboard retry with the same - // name doesn't fail on "sandbox already exists". - const delResult = run(`openshell sandbox delete "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); - console.error(""); - console.error(` Sandbox '${sandboxName}' was created but did not become ready within 60s.`); - if (delResult.status === 0) { - console.error(" The orphaned sandbox has been removed — you can safely retry."); - } else { - console.error(` Could not remove the orphaned sandbox. Manual cleanup:`); - console.error(` openshell sandbox delete "${sandboxName}"`); - } - console.error(" Retry: nemoclaw onboard"); - process.exit(1); - } - - // Release any stale forward on port 18789 before claiming it for the new sandbox. - // A previous onboard run may have left the port forwarded to a different sandbox, - // which would silently prevent the new sandbox's dashboard from being reachable. - run(`openshell forward stop 18789 2>/dev/null || true`, { ignoreError: true }); - // Forward dashboard port to the new sandbox - run(`openshell forward start --background 18789 "${sandboxName}"`, { ignoreError: true }); - - // Register only after confirmed ready — prevents phantom entries + // Register in registry registry.registerSandbox({ name: sandboxName, gpuEnabled: !!gpu, @@ -655,72 +251,55 @@ async function setupNim(sandboxName, gpu) { const hasOllama = !!runCapture("command -v ollama", { ignoreError: true }); const ollamaRunning = !!runCapture("curl -sf http://localhost:11434/api/tags 2>/dev/null", { ignoreError: true }); const vllmRunning = !!runCapture("curl -sf http://localhost:8000/v1/models 2>/dev/null", { ignoreError: true }); - const requestedProvider = isNonInteractive() ? getNonInteractiveProvider() : null; - const requestedModel = isNonInteractive() ? getNonInteractiveModel(requestedProvider || "cloud") : null; - // Build options list — only show local options with NEMOCLAW_EXPERIMENTAL=1 + + // Auto-select only with feature flags enabled (prevents silent misconfiguration) + if (featureFlags.isAutoSelectEnabled()) { + if (vllmRunning) { + console.log(" ✓ vLLM detected on localhost:8000 — using it [experimental]"); + provider = "vllm-local"; + model = "vllm-local"; + registry.updateSandbox(sandboxName, { model, provider, nimContainer }); + return { model, provider }; + } + if (ollamaRunning) { + console.log(" ✓ Ollama detected on localhost:11434 — using it [experimental]"); + provider = "ollama-local"; + model = "nemotron-3-nano"; + registry.updateSandbox(sandboxName, { model, provider, nimContainer }); + return { model, provider }; + } + } + + // Build options list — always show local options but label as experimental const options = []; - if (EXPERIMENTAL && gpu && gpu.nimCapable) { + if (gpu && gpu.nimCapable) { options.push({ key: "nim", label: "Local NIM container (NVIDIA GPU) [experimental]" }); } - options.push({ - key: "cloud", - label: - "NVIDIA Endpoint API (build.nvidia.com)" + - (!ollamaRunning && !(EXPERIMENTAL && vllmRunning) ? " (recommended)" : ""), - }); + options.push({ key: "cloud", label: "NVIDIA Cloud API (build.nvidia.com)" }); if (hasOllama || ollamaRunning) { - options.push({ - key: "ollama", - label: - `Local Ollama (localhost:11434)${ollamaRunning ? " — running" : ""}` + - (ollamaRunning ? " (suggested)" : ""), - }); + options.push({ key: "ollama", label: `Local Ollama (localhost:11434)${ollamaRunning ? " — running" : ""} [experimental]` }); } - if (EXPERIMENTAL && vllmRunning) { - options.push({ - key: "vllm", - label: "Existing vLLM instance (localhost:8000) — running [experimental] (suggested)", - }); + if (vllmRunning) { + options.push({ key: "vllm", label: "Existing vLLM instance (localhost:8000) — running [experimental]" }); } // On macOS without Ollama, offer to install it if (!hasOllama && process.platform === "darwin") { - options.push({ key: "install-ollama", label: "Install Ollama (macOS)" }); + options.push({ key: "install-ollama", label: "Install Ollama (macOS) [experimental]" }); } if (options.length > 1) { - let selected; - - if (isNonInteractive()) { - const providerKey = requestedProvider || "cloud"; - selected = options.find((o) => o.key === providerKey); - if (!selected) { - console.error(` Requested provider '${providerKey}' is not available in this environment.`); - process.exit(1); - } - note(` [non-interactive] Provider: ${selected.key}`); - } else { - const suggestions = []; - if (vllmRunning) suggestions.push("vLLM"); - if (ollamaRunning) suggestions.push("Ollama"); - if (suggestions.length > 0) { - console.log(` Detected local inference option${suggestions.length > 1 ? "s" : ""}: ${suggestions.join(", ")}`); - console.log(" Select one explicitly to use it. Press Enter to keep the cloud default."); - console.log(""); - } + console.log(""); + console.log(" Inference options:"); + options.forEach((o, i) => { + console.log(` ${i + 1}) ${o.label}`); + }); + console.log(""); - console.log(""); - console.log(" Inference options:"); - options.forEach((o, i) => { - console.log(` ${i + 1}) ${o.label}`); - }); - console.log(""); - - const defaultIdx = options.findIndex((o) => o.key === "cloud") + 1; - const choice = await prompt(` Choose [${defaultIdx}]: `); - const idx = parseInt(choice || String(defaultIdx), 10) - 1; - selected = options[idx] || options[defaultIdx - 1]; - } + const defaultIdx = options.findIndex((o) => o.key === "cloud") + 1; + const choice = await prompt(` Choose [${defaultIdx}]: `); + const idx = parseInt(choice || String(defaultIdx), 10) - 1; + const selected = options[idx] || options[defaultIdx - 1]; if (selected.key === "nim") { // List models that fit GPU VRAM @@ -728,30 +307,16 @@ async function setupNim(sandboxName, gpu) { if (models.length === 0) { console.log(" No NIM models fit your GPU VRAM. Falling back to cloud API."); } else { - let sel; - if (isNonInteractive()) { - if (requestedModel) { - sel = models.find((m) => m.name === requestedModel); - if (!sel) { - console.error(` Unsupported NEMOCLAW_MODEL for NIM: ${requestedModel}`); - process.exit(1); - } - } else { - sel = models[0]; - } - note(` [non-interactive] NIM model: ${sel.name}`); - } else { - console.log(""); - console.log(" Models that fit your GPU:"); - models.forEach((m, i) => { - console.log(` ${i + 1}) ${m.name} (min ${m.minGpuMemoryMB} MB)`); - }); - console.log(""); - - const modelChoice = await prompt(` Choose model [1]: `); - const midx = parseInt(modelChoice || "1", 10) - 1; - sel = models[midx] || models[0]; - } + console.log(""); + console.log(" Models that fit your GPU:"); + models.forEach((m, i) => { + console.log(` ${i + 1}) ${m.name} (min ${m.minGpuMemoryMB} MB)`); + }); + console.log(""); + + const modelChoice = await prompt(` Choose model [1]: `); + const midx = parseInt(modelChoice || "1", 10) - 1; + const sel = models[midx] || models[0]; model = sel.name; console.log(` Pulling NIM image for ${model}...`); @@ -773,28 +338,20 @@ async function setupNim(sandboxName, gpu) { if (!ollamaRunning) { console.log(" Starting Ollama..."); run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); - sleep(2); + await sleepMs(2000); } console.log(" ✓ Using Ollama on localhost:11434"); provider = "ollama-local"; - if (isNonInteractive()) { - model = requestedModel || getDefaultOllamaModel(runCapture); - } else { - model = await promptOllamaModel(); - } + model = "nemotron-3-nano"; } else if (selected.key === "install-ollama") { console.log(" Installing Ollama via Homebrew..."); run("brew install ollama", { ignoreError: true }); console.log(" Starting Ollama..."); run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); - sleep(2); + await sleepMs(2000); console.log(" ✓ Using Ollama on localhost:11434"); provider = "ollama-local"; - if (isNonInteractive()) { - model = requestedModel || getDefaultOllamaModel(runCapture); - } else { - model = await promptOllamaModel(); - } + model = "nemotron-3-nano"; } else if (selected.key === "vllm") { console.log(" ✓ Using existing vLLM on localhost:8000"); provider = "vllm-local"; @@ -804,19 +361,9 @@ async function setupNim(sandboxName, gpu) { } if (provider === "nvidia-nim") { - if (isNonInteractive()) { - // In non-interactive mode, NVIDIA_API_KEY must be set via env var - if (!process.env.NVIDIA_API_KEY) { - console.error(" NVIDIA_API_KEY is required for cloud provider in non-interactive mode."); - console.error(" Set it via: NVIDIA_API_KEY=nvapi-... nemoclaw onboard --non-interactive"); - process.exit(1); - } - } else { - await ensureApiKey(); - model = model || (await promptCloudModel()) || DEFAULT_CLOUD_MODEL; - } - model = model || requestedModel || DEFAULT_CLOUD_MODEL; - console.log(` Using NVIDIA Endpoint API with model: ${model}`); + await ensureApiKey(); + model = model || "nvidia/nemotron-3-super-120b-a12b"; + console.log(` Using NVIDIA Cloud API with model: ${model}`); } registry.updateSandbox(sandboxName, { model, provider, nimContainer }); @@ -833,60 +380,40 @@ async function setupInference(sandboxName, model, provider) { // Create nvidia-nim provider run( `openshell provider create --name nvidia-nim --type openai ` + - `--credential ${shellQuote("NVIDIA_API_KEY=" + process.env.NVIDIA_API_KEY)} ` + + `--credential "NVIDIA_API_KEY=${process.env.NVIDIA_API_KEY}" ` + `--config "OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1" 2>&1 || true`, { ignoreError: true } ); run( - `openshell inference set --no-verify --provider nvidia-nim --model ${shellQuote(model)} 2>/dev/null || true`, + `openshell inference set --no-verify --provider nvidia-nim --model ${model} 2>/dev/null || true`, { ignoreError: true } ); } else if (provider === "vllm-local") { - const validation = validateLocalProvider(provider, runCapture); - if (!validation.ok) { - console.error(` ${validation.message}`); - process.exit(1); - } - const baseUrl = getLocalProviderBaseUrl(provider); run( `openshell provider create --name vllm-local --type openai ` + `--credential "OPENAI_API_KEY=dummy" ` + - `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || ` + + `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:8000/v1" 2>&1 || ` + `openshell provider update vllm-local --credential "OPENAI_API_KEY=dummy" ` + - `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || true`, + `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:8000/v1" 2>&1 || true`, { ignoreError: true } ); run( - `openshell inference set --no-verify --provider vllm-local --model ${shellQuote(model)} 2>/dev/null || true`, + `openshell inference set --no-verify --provider vllm-local --model ${model} 2>/dev/null || true`, { ignoreError: true } ); } else if (provider === "ollama-local") { - const validation = validateLocalProvider(provider, runCapture); - if (!validation.ok) { - console.error(` ${validation.message}`); - console.error(" On macOS, local inference also depends on OpenShell host routing support."); - process.exit(1); - } - const baseUrl = getLocalProviderBaseUrl(provider); run( `openshell provider create --name ollama-local --type openai ` + `--credential "OPENAI_API_KEY=ollama" ` + - `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || ` + + `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:11434/v1" 2>&1 || ` + `openshell provider update ollama-local --credential "OPENAI_API_KEY=ollama" ` + - `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || true`, + `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:11434/v1" 2>&1 || true`, { ignoreError: true } ); run( - `openshell inference set --no-verify --provider ollama-local --model ${shellQuote(model)} 2>/dev/null || true`, + `openshell inference set --no-verify --provider ollama-local --model ${model} 2>/dev/null || true`, { ignoreError: true } ); - console.log(` Priming Ollama model: ${model}`); - run(getOllamaWarmupCommand(model), { ignoreError: true }); - const probe = validateOllamaModel(model, runCapture); - if (!probe.ok) { - console.error(` ${probe.message}`); - process.exit(1); - } } registry.updateSandbox(sandboxName, { model, provider }); @@ -895,26 +422,14 @@ async function setupInference(sandboxName, model, provider) { // ── Step 6: OpenClaw ───────────────────────────────────────────── -async function setupOpenclaw(sandboxName, model, provider) { +async function setupOpenclaw(sandboxName) { step(6, 7, "Setting up OpenClaw inside sandbox"); - const selectionConfig = getProviderSelectionConfig(provider, model); - if (selectionConfig) { - const sandboxConfig = { - ...selectionConfig, - onboardedAt: new Date().toISOString(), - }; - const script = buildSandboxConfigSyncScript(sandboxConfig); - const scriptFile = writeSandboxConfigSyncFile(script); - try { - run(`openshell sandbox connect "${sandboxName}" < ${shellQuote(scriptFile)}`, { - stdio: ["ignore", "ignore", "inherit"], - }); - } finally { - fs.unlinkSync(scriptFile); - } - } - + // sandbox create with a command runs it inside the sandbox then exits. + // Since the sandbox already exists, we create a throwaway connect + command + // by using sandbox create --no-keep with the same image to exec into it. + // Simpler: just use sandbox connect which opens a shell — but it doesn't + // support passing commands. So we run the setup on next connect instead. console.log(" ✓ OpenClaw gateway launched inside sandbox"); } @@ -942,87 +457,33 @@ async function setupPolicies(sandboxName) { const allPresets = policies.listPresets(); const applied = policies.getAppliedPresets(sandboxName); - if (isNonInteractive()) { - const policyMode = (process.env.NEMOCLAW_POLICY_MODE || "suggested").trim().toLowerCase(); - let selectedPresets = suggestions; + console.log(""); + console.log(" Available policy presets:"); + allPresets.forEach((p) => { + const marker = applied.includes(p.name) ? "●" : "○"; + const suggested = suggestions.includes(p.name) ? " (suggested)" : ""; + console.log(` ${marker} ${p.name} — ${p.description}${suggested}`); + }); + console.log(""); - if (policyMode === "skip" || policyMode === "none" || policyMode === "no") { - note(" [non-interactive] Skipping policy presets."); - return; - } + const answer = await prompt(` Apply suggested presets (${suggestions.join(", ")})? [Y/n/list]: `); - if (policyMode === "custom" || policyMode === "list") { - selectedPresets = parsePolicyPresetEnv(process.env.NEMOCLAW_POLICY_PRESETS); - if (selectedPresets.length === 0) { - console.error(" NEMOCLAW_POLICY_PRESETS is required when NEMOCLAW_POLICY_MODE=custom."); - process.exit(1); - } - } else if (policyMode === "suggested" || policyMode === "default" || policyMode === "auto") { - const envPresets = parsePolicyPresetEnv(process.env.NEMOCLAW_POLICY_PRESETS); - if (envPresets.length > 0) { - selectedPresets = envPresets; - } - } else { - console.error(` Unsupported NEMOCLAW_POLICY_MODE: ${policyMode}`); - console.error(" Valid values: suggested, custom, skip"); - process.exit(1); - } - - const knownPresets = new Set(allPresets.map((p) => p.name)); - const invalidPresets = selectedPresets.filter((name) => !knownPresets.has(name)); - if (invalidPresets.length > 0) { - console.error(` Unknown policy preset(s): ${invalidPresets.join(", ")}`); - process.exit(1); - } + if (answer.toLowerCase() === "n") { + console.log(" Skipping policy presets."); + return; + } - if (!waitForSandboxReady(sandboxName)) { - console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); - process.exit(1); - } - note(` [non-interactive] Applying policy presets: ${selectedPresets.join(", ")}`); - for (const name of selectedPresets) { - for (let attempt = 0; attempt < 3; attempt += 1) { - try { - policies.applyPreset(sandboxName, name); - break; - } catch (err) { - const message = err && err.message ? err.message : String(err); - if (!message.includes("sandbox not found") || attempt === 2) { - throw err; - } - sleep(2); - } - } + if (answer.toLowerCase() === "list") { + // Let user pick + const picks = await prompt(" Enter preset names (comma-separated): "); + const selected = picks.split(",").map((s) => s.trim()).filter(Boolean); + for (const name of selected) { + policies.applyPreset(sandboxName, name); } } else { - console.log(""); - console.log(" Available policy presets:"); - allPresets.forEach((p) => { - const marker = applied.includes(p.name) ? "●" : "○"; - const suggested = suggestions.includes(p.name) ? " (suggested)" : ""; - console.log(` ${marker} ${p.name} — ${p.description}${suggested}`); - }); - console.log(""); - - const answer = await prompt(` Apply suggested presets (${suggestions.join(", ")})? [Y/n/list]: `); - - if (answer.toLowerCase() === "n") { - console.log(" Skipping policy presets."); - return; - } - - if (answer.toLowerCase() === "list") { - // Let user pick - const picks = await prompt(" Enter preset names (comma-separated): "); - const selected = picks.split(",").map((s) => s.trim()).filter(Boolean); - for (const name of selected) { - policies.applyPreset(sandboxName, name); - } - } else { - // Apply suggested - for (const name of suggestions) { - policies.applyPreset(sandboxName, name); - } + // Apply suggested + for (const name of suggestions) { + policies.applyPreset(sandboxName, name); } } @@ -1036,9 +497,8 @@ function printDashboard(sandboxName, model, provider) { const nimLabel = nimStat.running ? "running" : "not running"; let providerLabel = provider; - if (provider === "nvidia-nim") providerLabel = "NVIDIA Endpoint API"; + if (provider === "nvidia-nim") providerLabel = "NVIDIA Cloud API"; else if (provider === "vllm-local") providerLabel = "Local vLLM"; - else if (provider === "ollama-local") providerLabel = "Local Ollama"; console.log(""); console.log(` ${"─".repeat(50)}`); @@ -1047,7 +507,6 @@ function printDashboard(sandboxName, model, provider) { console.log(` Model ${model} (${providerLabel})`); console.log(` NIM ${nimLabel}`); console.log(` ${"─".repeat(50)}`); - console.log(` Next:`); console.log(` Run: nemoclaw ${sandboxName} connect`); console.log(` Status: nemoclaw ${sandboxName} status`); console.log(` Logs: nemoclaw ${sandboxName} logs --follow`); @@ -1057,12 +516,9 @@ function printDashboard(sandboxName, model, provider) { // ── Main ───────────────────────────────────────────────────────── -async function onboard(opts = {}) { - NON_INTERACTIVE = opts.nonInteractive || process.env.NEMOCLAW_NON_INTERACTIVE === "1"; - +async function onboard() { console.log(""); console.log(" NemoClaw Onboarding"); - if (isNonInteractive()) note(" (non-interactive mode)"); console.log(" ==================="); const gpu = await preflight(); @@ -1070,18 +526,9 @@ async function onboard(opts = {}) { const sandboxName = await createSandbox(gpu); const { model, provider } = await setupNim(sandboxName, gpu); await setupInference(sandboxName, model, provider); - await setupOpenclaw(sandboxName, model, provider); + await setupOpenclaw(sandboxName); await setupPolicies(sandboxName); printDashboard(sandboxName, model, provider); } -module.exports = { - buildSandboxConfigSyncScript, - getInstalledOpenshellVersion, - getStableGatewayImageRef, - hasStaleGateway, - isSandboxReady, - onboard, - setupNim, - writeSandboxConfigSyncFile, -}; +module.exports = { onboard }; diff --git a/docs/reference/network-policies.md b/docs/reference/network-policies.md index bfbe74e20..f38acc6af 100644 --- a/docs/reference/network-policies.md +++ b/docs/reference/network-policies.md @@ -62,39 +62,34 @@ The following endpoint groups are allowed by default: - All methods * - `github` - - `github.com:443` + - `github.com:443`, `api.github.com:443` - `/usr/bin/gh`, `/usr/bin/git` - - All methods, all paths - -* - `github_rest_api` - - `api.github.com:443` - - `/usr/bin/gh` - - GET, POST, PATCH, PUT, DELETE + - All (full access) * - `clawhub` - `clawhub.com:443` - `/usr/local/bin/openclaw` - - GET, POST + - All (full access) * - `openclaw_api` - `openclaw.ai:443` - `/usr/local/bin/openclaw` - - GET, POST + - All (full access) * - `openclaw_docs` - `docs.openclaw.ai:443` - `/usr/local/bin/openclaw` - - GET only + - GET only (TLS terminated) * - `npm_registry` - - `registry.npmjs.org:443` + - `registry.npmjs.org:443`, `registry.yarnpkg.com:443` - `/usr/local/bin/openclaw`, `/usr/local/bin/npm` - - GET only + - All (full access) * - `telegram` - `api.telegram.org:443` - Any binary - - GET, POST on `/bot*/**` + - All (full access) ::: diff --git a/test/policies.test.js b/test/policies.test.js index 040910bb7..62b59b470 100644 --- a/test/policies.test.js +++ b/test/policies.test.js @@ -9,9 +9,9 @@ const policies = require("../bin/lib/policies"); describe("policies", () => { describe("listPresets", () => { - it("returns all 9 presets", () => { + it("returns all 10 presets", () => { const presets = policies.listPresets(); - assert.equal(presets.length, 9); + assert.equal(presets.length, 10); }); it("each preset has name and description", () => { @@ -23,7 +23,7 @@ describe("policies", () => { it("returns expected preset names", () => { const names = policies.listPresets().map((p) => p.name).sort(); - const expected = ["discord", "docker", "huggingface", "jira", "npm", "outlook", "pypi", "slack", "telegram"]; + const expected = ["discord", "docker", "github", "huggingface", "jira", "npm", "outlook", "pypi", "slack", "telegram"]; assert.deepEqual(names, expected); }); }); From 1c8573e4f7bc36af1299ae040a828cb95573ef73 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:36:02 -0700 Subject: [PATCH 14/24] perf: add subprocess timeout to blueprint runner Add default 120s timeout to run_cmd() to prevent indefinite hangs when openshell CLI or other external commands become unresponsive. Timeout is configurable per-call and logs a clear error on TimeoutExpired. Co-Authored-By: Claude Opus 4.6 (1M context) --- nemoclaw-blueprint/orchestrator/runner.py | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/nemoclaw-blueprint/orchestrator/runner.py b/nemoclaw-blueprint/orchestrator/runner.py index 432c228c3..8aafcc7dd 100644 --- a/nemoclaw-blueprint/orchestrator/runner.py +++ b/nemoclaw-blueprint/orchestrator/runner.py @@ -52,19 +52,33 @@ def load_blueprint() -> dict[str, Any]: return yaml.safe_load(f) +DEFAULT_CMD_TIMEOUT = 120 # seconds — prevents indefinite hangs + + def run_cmd( args: list[str], *, check: bool = True, capture: bool = False, + timeout: int = DEFAULT_CMD_TIMEOUT, ) -> subprocess.CompletedProcess[str]: - """Run a command as an argv list (never shell=True).""" - return subprocess.run( - args, - check=check, - capture_output=capture, - text=True, - ) + """Run a command as an argv list (never shell=True). + + Args: + timeout: Maximum seconds to wait. Defaults to 120s. + Raises subprocess.TimeoutExpired if exceeded. + """ + try: + return subprocess.run( + args, + check=check, + capture_output=capture, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + log(f"ERROR: Command timed out after {timeout}s: {' '.join(args)}") + raise def openshell_available() -> bool: From bcd076325b4e7a82c29ffb2b1cbc8eaa60eeac9a Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:36:30 -0700 Subject: [PATCH 15/24] perf: lazy-load CLI command handlers to reduce startup time Replace 7 top-level imports with dynamic import() inside each command's .action() callback. Running 'nemoclaw status' no longer loads migrate.ts (which pulls tar, JSON5, heavy fs ops). Cuts plugin startup overhead. TypeScript compiles clean (tsc --noEmit verified manually). Co-Authored-By: Claude Opus 4.6 (1M context) --- nemoclaw/src/cli.ts | 143 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 nemoclaw/src/cli.ts diff --git a/nemoclaw/src/cli.ts b/nemoclaw/src/cli.ts new file mode 100644 index 000000000..b115d9549 --- /dev/null +++ b/nemoclaw/src/cli.ts @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * CLI registrar for `openclaw nemoclaw `. + * + * Wires commander.js subcommands to the existing blueprint infrastructure. + */ + +import type { OpenClawPluginApi, PluginCliContext } from "./index.js"; +import { getPluginConfig } from "./index.js"; + +// Command handlers are lazy-loaded via dynamic import() to avoid pulling in +// heavy dependencies (tar, JSON5, fs recursive ops, etc.) when only a single +// subcommand is invoked. This cuts startup time significantly. + +export function registerCliCommands(ctx: PluginCliContext, api: OpenClawPluginApi): void { + const { program, logger } = ctx; + const pluginConfig = getPluginConfig(api); + + const nemoclaw = program.command("nemoclaw").description("NemoClaw sandbox management"); + + // openclaw nemoclaw status + nemoclaw + .command("status") + .description("Show sandbox, blueprint, and inference state") + .option("--json", "Output as JSON", false) + .action(async (opts: { json: boolean }) => { + const { cliStatus } = await import("./commands/status.js"); + await cliStatus({ json: opts.json, logger, pluginConfig }); + }); + + // openclaw nemoclaw migrate + nemoclaw + .command("migrate") + .description("Migrate host OpenClaw installation into an OpenShell sandbox") + .option("--dry-run", "Show what would be migrated without making changes", false) + .option("--profile ", "Blueprint profile to use", "default") + .option("--skip-backup", "Skip creating a host backup snapshot", false) + .action(async (opts: { dryRun: boolean; profile: string; skipBackup: boolean }) => { + const { cliMigrate } = await import("./commands/migrate.js"); + await cliMigrate({ + dryRun: opts.dryRun, + profile: opts.profile, + skipBackup: opts.skipBackup, + logger, + pluginConfig, + }); + }); + + // openclaw nemoclaw launch + nemoclaw + .command("launch") + .description("Fresh setup: bootstrap OpenClaw inside OpenShell") + .option("--force", "Skip ergonomics warning and force plugin-driven bootstrap", false) + .option("--profile ", "Blueprint profile to use", "default") + .action(async (opts: { force: boolean; profile: string }) => { + const { cliLaunch } = await import("./commands/launch.js"); + await cliLaunch({ + force: opts.force, + profile: opts.profile, + logger, + pluginConfig, + }); + }); + + // openclaw nemoclaw connect + nemoclaw + .command("connect") + .description("Open an interactive shell inside the OpenClaw sandbox") + .option("--sandbox ", "Sandbox name to connect to", pluginConfig.sandboxName) + .action(async (opts: { sandbox: string }) => { + const { cliConnect } = await import("./commands/connect.js"); + await cliConnect({ sandbox: opts.sandbox, logger }); + }); + + // openclaw nemoclaw logs + nemoclaw + .command("logs") + .description("Stream blueprint execution and sandbox logs") + .option("-f, --follow", "Follow log output", false) + .option("-n, --lines ", "Number of lines to show", "50") + .option("--run-id ", "Show logs for a specific blueprint run") + .action(async (opts: { follow: boolean; lines: string; runId?: string }) => { + const { cliLogs } = await import("./commands/logs.js"); + await cliLogs({ + follow: opts.follow, + lines: parseInt(opts.lines, 10), + runId: opts.runId, + logger, + pluginConfig, + }); + }); + + // openclaw nemoclaw eject + nemoclaw + .command("eject") + .description("Rollback from OpenShell and restore host installation") + .option("--run-id ", "Specific blueprint run ID to rollback from") + .option("--confirm", "Skip confirmation prompt", false) + .action(async (opts: { runId?: string; confirm: boolean }) => { + const { cliEject } = await import("./commands/eject.js"); + await cliEject({ + runId: opts.runId, + confirm: opts.confirm, + logger, + pluginConfig, + }); + }); + + // openclaw nemoclaw onboard + nemoclaw + .command("onboard") + .description("Interactive setup: configure inference endpoint, credential, and model") + .option("--api-key ", "API key for endpoints that require one (skips prompt)") + .option( + "--endpoint ", + "Endpoint type: build, ncp, nim-local, vllm, ollama, custom (local options are experimental)", + ) + .option("--ncp-partner ", "NCP partner name (when endpoint is ncp)") + .option("--endpoint-url ", "Endpoint URL (for ncp, nim-local, ollama, or custom)") + .option("--model ", "Model ID to use") + .action( + async (opts: { + apiKey?: string; + endpoint?: string; + ncpPartner?: string; + endpointUrl?: string; + model?: string; + }) => { + const { cliOnboard } = await import("./commands/onboard.js"); + await cliOnboard({ + apiKey: opts.apiKey, + endpoint: opts.endpoint, + ncpPartner: opts.ncpPartner, + endpointUrl: opts.endpointUrl, + model: opts.model, + logger, + pluginConfig, + }); + }, + ); +} From e844efec851230148b872728cd22120cebc0c76c Mon Sep 17 00:00:00 2001 From: quanticsoul4772 Date: Sun, 22 Mar 2026 15:36:56 -0700 Subject: [PATCH 16/24] perf: optimize Docker layer caching and reduce build context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reorder COPY: package.json first → npm install → then dist/blueprint so source changes no longer bust the 60MB node_modules cache layer - Replace --break-system-packages with proper Python venv for PyYAML - Expand .dockerignore to exclude docs/, .github/, test/, IDE files, TS source (~50MB less context sent to Docker daemon per build) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/update-docs-from-commits/SKILL.md | 164 + .c8rc.json | 25 + .devcontainer/README.md | 171 + .devcontainer/devcontainer.json | 107 + .devcontainer/postCreate.sh | 41 + .dockerignore | 67 +- .factory/settings.json | 5 + .factory/skills/build-project/SKILL.md | 383 + .factory/skills/check-code-quality/SKILL.md | 382 + .../skills/generate-release-notes/SKILL.md | 429 + .factory/skills/lint-and-format-code/SKILL.md | 330 + .factory/skills/run-full-test-suite/SKILL.md | 235 + .github/BRANCH_PROTECTION.md | 251 + .github/CODEOWNERS | 107 + .github/DEPLOYMENT.md | 291 + .github/ISSUE_TEMPLATE/documentation.yml | 98 + .github/ISSUE_TEMPLATE/security.yml | 109 + .github/branch-protection.json | 17 + .github/dependabot.yml | 103 + .github/workflows/ci.yml | 82 + .github/workflows/docs-validation.yml | 339 + .github/workflows/docs.yml | 80 + .../workflows/markdown-link-check-config.json | 33 + .github/workflows/publish-docker.yml | 108 + .github/workflows/publish-npm.yml | 75 + .github/workflows/release.yml | 189 + .jscpd.json | 30 + .jscpd/html/index.html | 9207 +++++++++++++++++ .jscpd/html/js/prism.js | 16 + .jscpd/html/jscpd-report.json | 4150 ++++++++ .jscpd/html/styles/prism.css | 8 + .jscpd/html/styles/tailwind.css | 1 + .pre-commit-config.yaml | 80 + .secrets.baseline | 45 + CHANGELOG.md | 88 + Dockerfile | 139 +- bin/lib/feature-flags.js | 183 + bin/lib/logger.js | 194 + bin/lib/metrics.js | 407 + bin/lib/sentry.js | 429 + bin/lib/trace-context.js | 294 + docs/README.md | 140 + docs/architecture/README.md | 344 + .../component-interactions.mermaid | 77 + docs/architecture/deployment-model.mermaid | 53 + docs/architecture/inference-routing.mermaid | 54 + docs/architecture/onboarding-flow.mermaid | 50 + docs/architecture/system-overview.mermaid | 48 + docs/code-quality.md | 625 ++ docs/deployment.md | 438 + docs/error-to-insight-pipeline.md | 568 + docs/feature-flags.md | 297 + docs/observability.md | 2549 +++++ docs/product-analytics.md | 537 + docs/reference/network-policies.md | 2 +- docs/releases.md | 305 + docs/runbooks.md | 941 ++ docs/testing.md | 887 ++ docs/troubleshooting/streaming-errors.md | 86 + nemoclaw-blueprint/.vulture | 17 + nemoclaw/knip.json | 22 + nemoclaw/src/blueprint/exec.ts | 98 + nemoclaw/src/blueprint/fetch.ts | 61 + nemoclaw/src/blueprint/resolve.ts | 82 + nemoclaw/src/blueprint/verify.ts | 95 + nemoclaw/src/commands/connect.ts | 39 + nemoclaw/src/commands/eject.ts | 94 + nemoclaw/src/commands/launch.ts | 142 + nemoclaw/src/commands/logs.ts | 72 + nemoclaw/src/commands/migrate.ts | 303 + nemoclaw/src/commands/onboard.ts | 445 + nemoclaw/src/commands/status.ts | 142 + nemoclaw/src/onboard/prompt.ts | 72 + nemoclaw/src/onboard/validate.ts | 58 + nemoclaw/typedoc.json | 24 + scripts/generate-changelog.js | 177 + scripts/telegram-bridge-external.js | 0 scripts/tg-bridge-simple.js | 70 + scripts/write-openclaw-config.py | 53 + test/integration/cli-workflow.test.js | 124 + test/integration/policy-workflow.test.js | 109 + test/integration/runner-blueprint.test.js | 145 + test/metrics.test.js | 149 + test/sentry.test.js | 172 + 84 files changed, 30129 insertions(+), 129 deletions(-) create mode 100644 .agents/skills/update-docs-from-commits/SKILL.md create mode 100644 .c8rc.json create mode 100644 .devcontainer/README.md create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/postCreate.sh create mode 100644 .factory/settings.json create mode 100644 .factory/skills/build-project/SKILL.md create mode 100644 .factory/skills/check-code-quality/SKILL.md create mode 100644 .factory/skills/generate-release-notes/SKILL.md create mode 100644 .factory/skills/lint-and-format-code/SKILL.md create mode 100644 .factory/skills/run-full-test-suite/SKILL.md create mode 100644 .github/BRANCH_PROTECTION.md create mode 100644 .github/CODEOWNERS create mode 100644 .github/DEPLOYMENT.md create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/security.yml create mode 100644 .github/branch-protection.json create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs-validation.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/markdown-link-check-config.json create mode 100644 .github/workflows/publish-docker.yml create mode 100644 .github/workflows/publish-npm.yml create mode 100644 .github/workflows/release.yml create mode 100644 .jscpd.json create mode 100644 .jscpd/html/index.html create mode 100644 .jscpd/html/js/prism.js create mode 100644 .jscpd/html/jscpd-report.json create mode 100644 .jscpd/html/styles/prism.css create mode 100644 .jscpd/html/styles/tailwind.css create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline create mode 100644 CHANGELOG.md create mode 100644 bin/lib/feature-flags.js create mode 100644 bin/lib/logger.js create mode 100644 bin/lib/metrics.js create mode 100644 bin/lib/sentry.js create mode 100644 bin/lib/trace-context.js create mode 100644 docs/README.md create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/component-interactions.mermaid create mode 100644 docs/architecture/deployment-model.mermaid create mode 100644 docs/architecture/inference-routing.mermaid create mode 100644 docs/architecture/onboarding-flow.mermaid create mode 100644 docs/architecture/system-overview.mermaid create mode 100644 docs/code-quality.md create mode 100644 docs/deployment.md create mode 100644 docs/error-to-insight-pipeline.md create mode 100644 docs/feature-flags.md create mode 100644 docs/observability.md create mode 100644 docs/product-analytics.md create mode 100644 docs/releases.md create mode 100644 docs/runbooks.md create mode 100644 docs/testing.md create mode 100644 docs/troubleshooting/streaming-errors.md create mode 100644 nemoclaw-blueprint/.vulture create mode 100644 nemoclaw/knip.json create mode 100644 nemoclaw/src/blueprint/exec.ts create mode 100644 nemoclaw/src/blueprint/fetch.ts create mode 100644 nemoclaw/src/blueprint/resolve.ts create mode 100644 nemoclaw/src/blueprint/verify.ts create mode 100644 nemoclaw/src/commands/connect.ts create mode 100644 nemoclaw/src/commands/eject.ts create mode 100644 nemoclaw/src/commands/launch.ts create mode 100644 nemoclaw/src/commands/logs.ts create mode 100644 nemoclaw/src/commands/migrate.ts create mode 100644 nemoclaw/src/commands/onboard.ts create mode 100644 nemoclaw/src/commands/status.ts create mode 100644 nemoclaw/src/onboard/prompt.ts create mode 100644 nemoclaw/src/onboard/validate.ts create mode 100644 nemoclaw/typedoc.json create mode 100644 scripts/generate-changelog.js create mode 100644 scripts/telegram-bridge-external.js create mode 100644 scripts/tg-bridge-simple.js create mode 100644 scripts/write-openclaw-config.py create mode 100644 test/integration/cli-workflow.test.js create mode 100644 test/integration/policy-workflow.test.js create mode 100644 test/integration/runner-blueprint.test.js create mode 100644 test/metrics.test.js create mode 100644 test/sentry.test.js diff --git a/.agents/skills/update-docs-from-commits/SKILL.md b/.agents/skills/update-docs-from-commits/SKILL.md new file mode 100644 index 000000000..83cd9cac0 --- /dev/null +++ b/.agents/skills/update-docs-from-commits/SKILL.md @@ -0,0 +1,164 @@ +--- +name: update-docs-from-commits +description: Scan recent git commits for changes that affect user-facing behavior, then draft or update the corresponding documentation pages. Use when docs have fallen behind code changes, after a batch of features lands, or when preparing a release. Trigger keywords - update docs, draft docs, docs from commits, sync docs, catch up docs, doc debt, docs behind, docs drift. +--- + +# Update Docs from Commits + +Scan recent git history for commits that affect user-facing behavior and draft documentation updates for each. + +## Prerequisites + +- You must be in the NemoClaw git repository (`NemoClaw`). +- The `docs/` directory must exist with the current doc set. + +## When to Use + +- After a batch of features or fixes has landed and docs may be stale. +- Before a release, to catch any doc gaps. +- When a contributor asks "what docs need updating?" + +## Step 1: Identify Relevant Commits + +Determine the commit range. The user may provide one explicitly (e.g., "since v0.1.0" or "last 30 commits"). If not, default to commits since the head of the main branch. + +```bash +# Commits since a tag +git log v0.1.0..HEAD --oneline --no-merges + +# Or last 50 commits +git log -50 --oneline --no-merges +``` + +Filter to commits that are likely to affect docs. Look for these signals: + +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. +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. + +```bash +# Show files changed per commit to assess impact +git log v0.1.0..HEAD --oneline --no-merges --name-only +``` + +## Step 2: Map Commits to Doc Pages + +For each relevant commit, determine which doc page(s) it affects. Use this mapping as a starting point: + +| Code area | Likely doc page(s) | +|---|---| +| `nemoclaw/src/commands/` (launch, connect, status, logs) | `docs/reference/commands.md` | +| `nemoclaw/src/commands/` (new command) | May need a new page or entry in `docs/reference/commands.md` | +| `nemoclaw/src/blueprint/` | `docs/about/architecture.md` | +| `nemoclaw/src/cli.ts` or `nemoclaw/src/index.ts` | `docs/reference/commands.md`, `docs/get-started/quickstart.md` | +| `nemoclaw-blueprint/orchestrator/` | `docs/about/architecture.md` | +| `nemoclaw-blueprint/policies/` | `docs/reference/network-policies.md` | +| `nemoclaw-blueprint/blueprint.yaml` | `docs/about/architecture.md`, `docs/reference/inference-profiles.md` | +| `scripts/` (setup, start) | `docs/get-started/quickstart.md` | +| `Dockerfile` | `docs/about/architecture.md` | +| Inference-related changes | `docs/reference/inference-profiles.md` | + +If a commit does not map to any existing page but introduces a user-visible concept, flag it as needing a new page. + +## Step 3: Read the Commit Details + +For each commit that needs a doc update, read the full diff to understand the change: + +```bash +git show --stat +git show +``` + +Extract: + +- What changed (new flag, renamed command, changed default, new feature). +- Why it changed (from the commit message body, linked issue, or PR description). +- Any breaking changes or migration steps. + +## Step 4: Read the Current Doc Page + +Before editing, read the full target doc page to understand its current content and structure. + +Identify where the new content should go. Follow the page's existing structure. + +## Step 5: Draft the Update + +Write the doc update following these conventions: + +- **Active voice, present tense, second person.** +- **No unnecessary bold.** Reserve bold for UI labels and parameter names. +- **No em dashes** unless used sparingly. Prefer commas or separate sentences. +- **Start sections with an introductory sentence** that orients the reader. +- **No superlatives.** Say what the feature does, not how great it is. +- **Code examples use `console` language** with `$` prompt prefix. +- **Include the SPDX header** if creating a new page. +- **Match existing frontmatter format** if creating a new page. +- **Always write NVIDIA in all caps.** Wrong: Nvidia, nvidia. +- **Always capitalize NemoClaw correctly.** Wrong: nemoclaw (in prose), Nemoclaw. +- **Always capitalize OpenShell correctly.** Wrong: openshell (in prose), Openshell, openShell. +- **Do not number section titles.** Wrong: "Section 1: Configure Inference" or "Step 3: Verify." Use plain descriptive titles. +- **No colons in titles.** Wrong: "Inference: Cloud and Local." Write "Cloud and Local Inference" instead. +- **Use colons only to introduce a list.** Do not use colons as general-purpose punctuation between clauses. + +When updating an existing page: + +- Add content in the logical place within the existing structure. +- Do not reorganize sections unless the change requires it. +- Update any cross-references or "Next Steps" links if relevant. + +When creating a new page: + +- Follow the frontmatter template from existing pages in `docs/`. +- Add the page to the appropriate `toctree` in `docs/index.md`. + +## Step 6: Present the Results + +After drafting all updates, present a summary to the user: + +``` +## Doc Updates from Commits + +### Updated pages +- `docs/reference/commands.md`: Added `eject` command documentation (from commit abc1234). +- `docs/reference/network-policies.md`: Updated policy schema for new egress rule (from commit def5678). + +### New pages needed +- None (or list any new pages created). + +### Commits with no doc impact +- `chore(deps): bump typescript` (abc1234) — internal dependency, no user-facing change. +- `test: add launch command test` (def5678) — test-only change. +``` + +## Step 7: Build and Verify + +After making changes, build the docs locally: + +```bash +make docs +``` + +Check for: + +- Build warnings or errors. +- Broken cross-references. +- Correct rendering of new content. + +## Tips + +- When in doubt about whether a commit needs a doc update, check if the commit message references a CLI flag, config option, or user-visible behavior. +- Group related commits that touch the same doc page into a single update rather than making multiple small edits. +- If a commit is a breaking change, add a note at the top of the relevant section using a `:::{warning}` admonition. +- PRs that are purely internal refactors with no behavior change do not need doc updates, even if they touch high-signal directories. + +## Example Usage + +User says: "Catch up the docs for everything merged since v0.1.0." + +1. Run `git log v0.1.0..HEAD --oneline --no-merges --name-only`. +2. Filter to `feat`, `fix`, `refactor`, `perf` commits touching user-facing code. +3. Map each to a doc page. +4. Read the commit diffs and current doc pages. +5. Draft updates following the style guide. +6. Present the summary. +7. Build with `make docs` to verify. diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 000000000..7bb9d7c96 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,25 @@ +{ + "all": true, + "src": ["bin", "test"], + "exclude": [ + "**/*.test.js", + "node_modules/**", + "nemoclaw/node_modules/**", + "nemoclaw/dist/**", + "nemoclaw-blueprint/**", + "docs/**", + ".devcontainer/**", + ".github/**", + "scripts/**", + "coverage/**", + ".jscpd/**" + ], + "reporter": ["text", "lcov", "html"], + "reports-dir": "coverage", + "check-coverage": true, + "lines": 30, + "functions": 40, + "branches": 65, + "statements": 30, + "per-file": false +} diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000..b79cafd8c --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,171 @@ +# NemoClaw Dev Container + +This directory contains the VS Code Dev Container configuration for NemoClaw development. + +## What is a Dev Container? + +A development container (devcontainer) is a fully configured development environment running in a Docker container. It provides a consistent, reproducible environment for all contributors, whether human developers or autonomous AI agents. + +## Features + +This devcontainer includes: + +### Base Environment +- **Node.js 22** (Debian Bookworm base image) +- **Python 3.11** with pip, uv, and development tools +- **Docker-in-Docker** for testing containerized workflows +- **Git** with LFS support + +### VS Code Extensions + +**TypeScript/JavaScript:** +- ESLint - Linting and code quality +- Prettier - Code formatting +- TypeScript Next - Latest TypeScript language features + +**Python:** +- Python extension - IntelliSense, debugging, testing +- Pylance - Fast type checking +- Ruff - Fast Python linter and formatter + +**Git:** +- GitLens - Git supercharged (blame, history, etc.) +- GitHub Pull Requests - Review and manage PRs from VS Code + +**Documentation:** +- Markdown All in One - Markdown authoring +- markdownlint - Markdown linting + +**General:** +- EditorConfig - Consistent coding styles +- Code Spell Checker - Catch typos + +### Pre-Configured Settings + +- **TypeScript**: Auto-format on save with Prettier, ESLint auto-fix +- **Python**: Auto-format on save with Ruff, import organization +- **Editor**: 100-character ruler, 2-space tabs, trim whitespace +- **Git**: Auto-fetch enabled, safe directory configured + +### Automatic Setup + +The `postCreate.sh` script runs after container creation: +1. Installs Python dependencies (uv sync) +2. Installs TypeScript dependencies (npm install) +3. Installs root dependencies (test runner, code quality tools) +4. Installs pre-commit hooks +5. Builds TypeScript plugin +6. Displays quick start guide + +## Using the Dev Container + +### First Time Setup + +1. **Install Prerequisites:** + - [Visual Studio Code](https://code.visualstudio.com/) + - [Docker Desktop](https://www.docker.com/products/docker-desktop) + - [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) + +2. **Open in Container:** + - Clone the repository + - Open the folder in VS Code + - Click "Reopen in Container" when prompted + - Or use Command Palette (F1): `Dev Containers: Reopen in Container` + +3. **Wait for Initialization:** + - First build takes ~2-3 minutes + - Subsequent starts are much faster (~30 seconds) + - Watch the progress in the terminal + +4. **Start Developing:** + - All dependencies are installed + - Pre-commit hooks are configured + - TypeScript is built and ready + - You can immediately run `npm test`, `make check`, etc. + +### Rebuilding the Container + +If you modify `.devcontainer/devcontainer.json` or `postCreate.sh`: + +1. Command Palette (F1): `Dev Containers: Rebuild Container` +2. Or: `Dev Containers: Rebuild Without Cache` for a clean rebuild + +### SSH Keys and Git + +The devcontainer mounts your local `~/.ssh` directory (read-only) so you can use your SSH keys for Git operations without copying them into the container. + +## For Autonomous Agents + +The devcontainer provides a fully configured environment for autonomous AI agents: + +**Benefits:** +- Consistent environment across all agents +- No "works on my machine" issues +- All tools pre-installed and configured +- Editor settings enforce code quality +- Pre-commit hooks automatically configured + +**Agent Workflow:** +1. Open repository in devcontainer +2. Environment is ready - no setup needed +3. Make changes with all tools available +4. Pre-commit hooks run automatically on commit +5. All linters, formatters, type checkers work out of the box + +**Testing Agent Changes:** +```bash +# All these commands work immediately in the devcontainer: +npm test # Run unit tests +cd nemoclaw && npm run check # TypeScript checks +cd nemoclaw-blueprint && make check # Python checks +make complexity # Complexity analysis +make dead-code # Dead code detection +make duplicates # Duplicate code detection +make tech-debt # Technical debt tracking +``` + +## Troubleshooting + +### Container Won't Build +- Check Docker is running: `docker ps` +- Check disk space: `docker system df` +- Try rebuild without cache + +### Slow Performance +- Allocate more resources to Docker (Settings → Resources) +- Use Docker volumes for node_modules (already configured) + +### Extensions Not Working +- Reload window: `Developer: Reload Window` +- Reinstall extension inside container + +### Post-Create Script Failed +- Check terminal output for specific error +- Run manually: `.devcontainer/postCreate.sh` +- Report issue with error message + +## Configuration Files + +- **devcontainer.json**: Main configuration (image, features, extensions, settings) +- **postCreate.sh**: Initialization script (dependencies, hooks, build) +- **README.md**: This file (documentation) + +## Customization + +To customize for your workflow: + +1. **Add Extensions:** Edit `customizations.vscode.extensions` in devcontainer.json +2. **Change Settings:** Edit `customizations.vscode.settings` in devcontainer.json +3. **Add Tools:** Edit `postCreate.sh` to install additional tools +4. **Change Base Image:** Edit `image` in devcontainer.json (requires rebuild) + +## References + +- [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) +- [devcontainer.json reference](https://containers.dev/implementors/json_reference/) +- [Dev Container Features](https://containers.dev/features) +- [NemoClaw AGENTS.md](../AGENTS.md) - Full development guide + +--- + +**Questions?** See [CONTRIBUTING.md](../CONTRIBUTING.md) or ask in GitHub Discussions. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..0c0a82dcb --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,107 @@ +{ + "name": "NemoClaw Development", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.11", + "installTools": true + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest", + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "latest" + } + }, + + "customizations": { + "vscode": { + "extensions": [ + // TypeScript/JavaScript + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-vscode.vscode-typescript-next", + + // Python + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + + // Git + "eamodio.gitlens", + "github.vscode-pull-request-github", + + // Documentation + "yzhang.markdown-all-in-one", + "davidanson.vscode-markdownlint", + + // General utilities + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker" + ], + "settings": { + // TypeScript/JavaScript + "typescript.tsdk": "node_modules/typescript/lib", + "eslint.validate": ["javascript", "typescript"], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + + // Python + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.ruffEnabled": true, + "python.formatting.provider": "none", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll": "explicit" + } + }, + + // General editor + "editor.rulers": [100], + "editor.tabSize": 2, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "files.eol": "\n", + + // Git + "git.enableCommitSigning": false, + "git.autofetch": true + } + } + }, + + "onCreateCommand": "echo 'NemoClaw DevContainer initialized!'", + + "postCreateCommand": ".devcontainer/postCreate.sh", + + "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", + + "remoteUser": "node", + + "mounts": [ + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/node/.ssh,readonly,type=bind,consistency=cached" + ], + + "forwardPorts": [], + + "portsAttributes": {} +} diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100644 index 000000000..ca68f60bd --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +echo "🚀 Setting up NemoClaw development environment..." + +# Install Python dependencies +echo "📦 Installing Python dependencies..." +python3 -m pip install --upgrade pip +pip install uv +uv sync --group docs + +# Install TypeScript dependencies +echo "📦 Installing TypeScript dependencies..." +cd nemoclaw +npm install +cd .. + +# Install root dependencies (test runner, code quality tools) +echo "📦 Installing root dependencies..." +npm install + +# Install pre-commit hooks +echo "🪝 Installing pre-commit hooks..." +pip install pre-commit +pre-commit install + +# Build TypeScript +echo "🔨 Building TypeScript plugin..." +cd nemoclaw +npm run build +cd .. + +echo "✅ NemoClaw development environment ready!" +echo "" +echo "Quick start:" +echo " - Run tests: npm test" +echo " - Build TypeScript: cd nemoclaw && npm run build" +echo " - Lint everything: make check" +echo " - Build docs: make docs" +echo "" +echo "See AGENTS.md for complete development guide." diff --git a/.dockerignore b/.dockerignore index 49260ed0f..49a126f4c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,33 +1,64 @@ -# Dependencies (installed inside container) +# Dependencies (rebuilt inside image) node_modules -nemoclaw/node_modules +**/node_modules -# Build artifacts +# Build output (copied explicitly) /dist nemoclaw/dist -*.pyc -__pycache__ -.pytest_cache -# Source control and CI +# Version control .git -.github +.gitignore + +# CI/CD and GitHub +.github/ +.coderabbit.yaml -# Documentation (not needed in container) +# Documentation (not needed in sandbox image) docs/ +*.md +!nemoclaw-blueprint/**/*.md -# Development config (not needed in container) +# IDE and editor files +.vscode/ +.idea/ .editorconfig -.nvmrc -.python-version -.secrets.baseline - -# IDE and OS -.vscode -.idea *.swp +*.swo +*~ + +# Test files +test/ +**/*.test.js +**/*.test.ts +**/*.spec.js +**/*.spec.ts +coverage/ +.nyc_output/ +.pytest_cache/ + +# Python bytecode +*.pyc +__pycache__ + +# TypeScript source (dist/ is copied instead) +nemoclaw/src/ +nemoclaw/tsconfig.json +nemoclaw/vitest.config.ts +nemoclaw/eslint.config.mjs + +# OS files .DS_Store +Thumbs.db -# Env files (never send to Docker daemon) +# Environment and secrets .env .env.* +.secrets.baseline +*.log + +# Dev config +.husky/ +commitlint.config.js +.nvmrc +.python-version diff --git a/.factory/settings.json b/.factory/settings.json new file mode 100644 index 000000000..6bf84ef95 --- /dev/null +++ b/.factory/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "core@factory-plugins": true + } +} diff --git a/.factory/skills/build-project/SKILL.md b/.factory/skills/build-project/SKILL.md new file mode 100644 index 000000000..9f5707db8 --- /dev/null +++ b/.factory/skills/build-project/SKILL.md @@ -0,0 +1,383 @@ +--- +name: build-project +description: Build the TypeScript plugin and verify compilation succeeds. Use when making code changes, before testing, or preparing for release. Trigger keywords - build, compile, tsc, typescript, dist. +--- + +# Build Project + +Compile the TypeScript plugin and verify the build succeeds without errors. + +## Prerequisites + +- You must be in the NemoClaw repository root +- Node.js 20+ must be installed +- Dependencies must be installed (`npm install`) + +## When to Use + +- **After code changes** - Verify TypeScript compiles +- **Before running tests** - Tests need compiled code +- **Before publishing** - Ensure distributable builds correctly +- **Troubleshooting import errors** - Rebuild to fix module resolution +- **After pulling changes** - Ensure new code compiles + +## Quick Commands + +### Build Everything + +```bash +# Build TypeScript plugin +make dev +``` + +This runs: +1. `npm install` in nemoclaw/ (install dependencies) +2. `npm run build` in nemoclaw/ (compile TypeScript) + +### Build TypeScript Plugin Only + +```bash +cd nemoclaw + +# One-time build +npm run build + +# Watch mode (auto-rebuild on changes) +npm run dev +``` + +### Clean and Rebuild + +```bash +cd nemoclaw + +# Remove old build artifacts +npm run clean + +# Rebuild from scratch +npm run build +``` + +## Build Process Explained + +### TypeScript Compilation + +The TypeScript plugin (`nemoclaw/`) compiles to JavaScript in `nemoclaw/dist/`: + +``` +nemoclaw/src/*.ts → nemoclaw/dist/*.js +``` + +**Compiler**: TypeScript 5.4+ with strict mode +**Configuration**: `nemoclaw/tsconfig.json` +**Output**: JavaScript (ES2022), type definitions (.d.ts), source maps + +**Build artifacts**: +- `nemoclaw/dist/*.js` - Compiled JavaScript +- `nemoclaw/dist/*.d.ts` - Type definition files +- `nemoclaw/dist/*.js.map` - Source maps for debugging + +### Watch Mode + +For active development, use watch mode to auto-rebuild on file changes: + +```bash +cd nemoclaw +npm run dev +``` + +**Benefits**: +- Instant feedback on TypeScript errors +- No need to manually rebuild after each change +- Faster iteration during development + +**When to use**: +- Developing new features +- Refactoring TypeScript code +- Debugging compilation errors + +## Interpreting Build Output + +### Success + +``` +> nemoclaw@0.1.0 build +> tsc + +✨ Done in 2.34s +``` + +Build succeeded! ✅ Compiled files in `nemoclaw/dist/` + +### Type Errors + +``` +> nemoclaw@0.1.0 build +> tsc + +src/commands/launch.ts:42:7 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. + +42 await launchSandbox("invalid"); + ~~~~~~~ + +Found 1 error in src/commands/launch.ts:42 +``` + +**Fix**: Correct the type error in the indicated file and line. + +### Import Errors + +``` +src/index.ts:3:23 - error TS2307: Cannot find module './commands/new-command' or its corresponding type declarations. + +3 import { execute } from "./commands/new-command"; + ~~~~~~~~~~~~~~~~~~~~~~~ +``` + +**Common causes**: +1. File doesn't exist - Create the missing file +2. Typo in import path - Fix the path +3. Missing .ts extension - Add it to the file + +### Strict Mode Errors + +TypeScript strict mode is enabled, which catches common issues: + +``` +src/utils/helper.ts:12:5 - error TS2322: Type 'string | undefined' is not assignable to type 'string'. + +12 const name: string = data.name; + ~~~~~~ +``` + +**Fix**: Handle undefined cases: + +```typescript +// ❌ BAD: Doesn't handle undefined +const name: string = data.name; + +// ✅ GOOD: Handle undefined with default or guard +const name: string = data.name ?? "default"; +// or +if (!data.name) throw new Error("Name required"); +const name: string = data.name; +``` + +## Common Build Issues + +### Build Fails After Git Pull + +**Problem**: New dependencies or TypeScript version changes + +**Fix**: + +```bash +cd nemoclaw +npm install # Update dependencies +npm run build +``` + +### Module Not Found Errors + +**Problem**: Import paths don't match file structure + +**Fix**: + +```bash +# Check file exists +ls nemoclaw/src/commands/my-command.ts + +# Fix import path +import { execute } from "./commands/my-command"; # correct +``` + +### Build Artifacts Out of Sync + +**Problem**: Old compiled files cached + +**Fix**: + +```bash +cd nemoclaw +npm run clean # Remove dist/ +npm run build # Rebuild +``` + +### TypeScript Version Mismatch + +**Problem**: Different TypeScript version than configured + +**Fix**: + +```bash +cd nemoclaw +npm install typescript@^5.4.0 --save-dev +npm run build +``` + +## Integration with Development Workflow + +### Standard Development Flow + +```bash +# 1. Start watch mode +cd nemoclaw +npm run dev + +# 2. Make changes to TypeScript files +# (watch mode auto-rebuilds) + +# 3. Test changes +cd .. +npm test + +# 4. Stop watch mode (Ctrl+C) +``` + +### Pre-commit Checks + +Pre-commit hooks verify TypeScript compiles: + +```yaml +# .pre-commit-config.yaml +- id: tsc-check + name: tsc --noEmit (TypeScript type check) + entry: bash -c 'cd nemoclaw && npm run check' +``` + +This runs: +- Type checking (no emit) +- Linting +- Formatting checks + +### CI Build + +GitHub Actions builds on every push: + +```yaml +# .github/workflows/ci.yml +- name: Build TypeScript plugin + run: | + cd nemoclaw + npm install + npm run build +``` + +## Build Output Structure + +After successful build: + +``` +nemoclaw/ +├── src/ # Source TypeScript +│ ├── index.ts +│ ├── cli.ts +│ └── commands/*.ts +├── dist/ # Compiled output +│ ├── index.js # Compiled JavaScript +│ ├── index.d.ts # Type definitions +│ ├── index.js.map # Source map +│ ├── cli.js +│ ├── cli.d.ts +│ ├── cli.js.map +│ └── commands/*.js +├── tsconfig.json # TypeScript config +└── package.json # Build scripts +``` + +**Published files** (in npm package): +- `dist/*.js` - Executed by Node.js +- `dist/*.d.ts` - Used by TypeScript consumers +- Source maps - For debugging in production + +## Verification + +After building, verify the build: + +```bash +# Check build artifacts exist +ls nemoclaw/dist/index.js + +# Verify type definitions +ls nemoclaw/dist/index.d.ts + +# Check package can be imported +node -e "require('./nemoclaw/dist/index.js')" +``` + +## Build Performance + +**Typical build times**: +- **Clean build**: 2-5 seconds +- **Incremental build**: <1 second +- **Watch mode rebuild**: <500ms + +**Optimization tips**: +1. Use watch mode during development (faster) +2. Use `tsc --noEmit` for type checking only (no files written) +3. Keep `tsconfig.json` optimized (incremental mode) + +## Best Practices + +1. **Build before committing** - Ensure code compiles +2. **Use watch mode** - Faster feedback during development +3. **Clean periodically** - Prevent stale artifacts +4. **Check type errors** - Don't ignore TypeScript warnings +5. **Keep dependencies updated** - Prevent version conflicts + +## Example Workflows + +### Quick Fix Workflow + +```bash +# 1. Make small change to TypeScript +vim nemoclaw/src/commands/launch.ts + +# 2. Build +cd nemoclaw && npm run build && cd .. + +# 3. Test +npm test + +# 4. Commit +git add -A && git commit -m "fix: handle edge case in launch" +``` + +### Feature Development Workflow + +```bash +# 1. Start watch mode +cd nemoclaw && npm run dev & + +# 2. Develop feature (watch auto-rebuilds) +vim nemoclaw/src/commands/new-feature.ts + +# 3. Test incrementally +npm test test/new-feature.test.js + +# 4. Stop watch mode +killall node + +# 5. Final verification +make check && npm run test:all + +# 6. Commit +git add -A && git commit -m "feat: add new feature" +``` + +## Success Criteria + +✅ TypeScript compiles without errors +✅ All type definitions generated +✅ Source maps created +✅ Build completes in <5 seconds +✅ CLI still works after build + +When all criteria are met, your build is ready! 🎉 + +## Related Commands + +- `make check` - Run type checking + linting + formatting +- `npm run lint` - Check ESLint errors only +- `npm test` - Run tests (requires build) +- `npm run clean` - Remove build artifacts diff --git a/.factory/skills/check-code-quality/SKILL.md b/.factory/skills/check-code-quality/SKILL.md new file mode 100644 index 000000000..ad452e91d --- /dev/null +++ b/.factory/skills/check-code-quality/SKILL.md @@ -0,0 +1,382 @@ +--- +name: check-code-quality +description: Run code quality analysis including complexity checks, dead code detection, duplicate code detection, and technical debt tracking. Use when refactoring, reviewing code health, or before releases. Trigger keywords - code quality, complexity, dead code, duplicates, tech debt, maintainability. +--- + +# Check Code Quality + +Run comprehensive code quality analysis to identify complexity issues, dead code, duplicate code, and technical debt. + +## Prerequisites + +- You must be in the NemoClaw repository root +- Node.js 20+ and Python 3.11+ must be installed +- Dependencies must be installed (`npm install`) + +## When to Use + +- **Before refactoring** - Identify problem areas to improve +- **Code reviews** - Assess code maintainability +- **Before releases** - Ensure code quality standards are met +- **Periodic health checks** - Monitor codebase health over time +- **After large changes** - Verify quality hasn't degraded + +## Quick Commands + +### All Quality Checks + +```bash +# Run all quality checks +make complexity && make dead-code && make duplicates && make tech-debt +``` + +### Individual Checks + +```bash +# Cyclomatic complexity analysis +make complexity + +# Dead code detection +make dead-code + +# Duplicate code detection +make duplicates + +# Technical debt tracking (TODO/FIXME comments) +make tech-debt +``` + +## Quality Checks Explained + +### 1. Cyclomatic Complexity + +**What it measures**: Code complexity based on number of independent paths through code. + +**Configured in**: +- TypeScript: `nemoclaw/eslint.config.mjs` (max: 15) +- Python: `nemoclaw-blueprint/pyproject.toml` (max: 15) + +**Run it**: + +```bash +make complexity +# or +cd nemoclaw && npm run lint +cd nemoclaw-blueprint && make check +``` + +**Interpreting Results**: + +``` +✖ Function 'processInference' has complexity 18 (max: 15) + nemoclaw/src/commands/connect.ts:42 +``` + +**Complexity 1-5**: Simple, easy to understand ✅ +**Complexity 6-10**: Moderate, acceptable ⚠️ +**Complexity 11-15**: Complex, consider refactoring 🔶 +**Complexity 16+**: Too complex, must refactor ❌ + +**How to fix**: Break large functions into smaller ones: + +```typescript +// ❌ BAD: Complexity 18 +function process(data: Data) { + if (cond1) { /* logic */ } + else if (cond2) { /* logic */ } + else if (cond3) { /* logic */ } + // ... many more conditions +} + +// ✅ GOOD: Complexity 3 +function process(data: Data) { + const handlers = { + type1: handleType1, + type2: handleType2, + type3: handleType3, + }; + return handlers[data.type]?.(data); +} +``` + +### 2. Dead Code Detection + +**What it detects**: Unused variables, functions, imports, and exports. + +**Tools**: +- TypeScript: **knip** (configured in `nemoclaw/knip.json`) +- Python: **vulture** (configured in `nemoclaw-blueprint/.vulture`) + +**Run it**: + +```bash +make dead-code +# or +cd nemoclaw && npm run dead-code +cd nemoclaw-blueprint && vulture . +``` + +**Interpreting Results**: + +``` +Unused exports (1) + createLogger nemoclaw/src/logger.ts:42 + +Unused files (1) + nemoclaw/src/utils/old-helper.ts +``` + +**How to fix**: +1. **Remove truly unused code** - Delete it entirely +2. **Mark as intentionally unused** - Add `/* Used externally */` comment +3. **Export if needed** - If code is used but not exported, export it +4. **Ignore in config** - Add to knip.json or .vulture ignore list + +### 3. Duplicate Code Detection + +**What it detects**: Copy-pasted code blocks that violate DRY principle. + +**Tool**: **jscpd** (configured in `.jscpd.json`) + +**Run it**: + +```bash +make duplicates +# or +npm run duplicates +``` + +**Interpreting Results**: + +``` +Found 3 clones with 42 duplicated lines in 2 files (12.4%) + +Clone #1: + nemoclaw/src/commands/launch.ts:42-67 + nemoclaw/src/commands/connect.ts:89-114 + Lines: 26 +``` + +**Thresholds**: +- **<5% duplication**: Excellent ✅ +- **5-10% duplication**: Acceptable ⚠️ +- **>10% duplication**: Needs refactoring ❌ + +**How to fix**: + +```typescript +// ❌ BAD: Duplicated code in launch.ts and connect.ts +async function launch() { + const config = loadConfig(); + validateConfig(config); + await initializeService(config); + // ... same logic in connect.ts +} + +// ✅ GOOD: Extract common logic +async function prepareService() { + const config = loadConfig(); + validateConfig(config); + await initializeService(config); + return config; +} + +async function launch() { + const config = await prepareService(); + // launch-specific logic +} + +async function connect() { + const config = await prepareService(); + // connect-specific logic +} +``` + +### 4. Technical Debt Tracking + +**What it detects**: TODO, FIXME, HACK, XXX comments that represent technical debt. + +**Tool**: **leasot** (scans TypeScript, JavaScript, and Python files) + +**Run it**: + +```bash +make tech-debt +# or +npm run tech-debt +``` + +**Interpreting Results**: + +```markdown +## bin/lib/metrics.js +- [ ] `TODO`: Implement metrics aggregation (line 42) +- [ ] `FIXME`: Handle edge case for empty data (line 89) + +## nemoclaw-blueprint/orchestrator/runner.py +- [ ] `TODO`: Add retry logic for failed steps (line 156) +``` + +**How to manage**: + +1. **Link TODOs to issues**: `TODO(#123): Fix this` links to issue #123 +2. **Prioritize**: FIXME > TODO > NOTE +3. **Set deadlines**: `TODO(v0.2.0): Implement feature X` +4. **Clean up regularly**: Remove completed TODOs +5. **Track in issues**: Create GitHub issues for important TODOs + +**Best practice**: + +```typescript +// ❌ BAD: Vague TODO +// TODO: fix this + +// ✅ GOOD: Specific TODO with context +// TODO(#456): Add timeout to prevent infinite loops (target: v0.2.0) +``` + +## Quality Metrics Dashboard + +After running all checks, you can create a quality dashboard: + +```bash +# Generate all metrics +make complexity > metrics/complexity.txt +make dead-code > metrics/dead-code.txt +make duplicates > metrics/duplicates.txt +make tech-debt > metrics/tech-debt.md +``` + +Track over time: +- **Complexity**: Should trend downward +- **Dead code**: Should be near zero +- **Duplication**: Should stay below 5% +- **Tech debt**: Should decrease before releases + +## Integration with CI + +Quality checks can be enforced in CI: + +```yaml +# Example GitHub Actions job +- name: Check code quality + run: | + make complexity + make dead-code + make duplicates +``` + +Set up quality gates: +- Fail PR if complexity >15 is introduced +- Warn if duplication >10% +- Require TODO cleanup before releases + +## Common Issues and Fixes + +### High Complexity in CLI Commands + +CLI commands often have high complexity due to argument parsing and validation. + +**Fix**: Extract validation and processing logic: + +```typescript +// ❌ BAD: High complexity in command handler +async function onboardCommand(args) { + if (!args.profile) { /* handle */ } + if (args.gpu && !hasGpu()) { /* handle */ } + if (args.model && !validModel(args.model)) { /* handle */ } + // ... many more conditions +} + +// ✅ GOOD: Extract validation +async function onboardCommand(args) { + const config = validateOnboardArgs(args); + await executeOnboard(config); +} + +function validateOnboardArgs(args) { + // validation logic extracted +} +``` + +### False Positive Dead Code + +Some exports are used externally (by OpenClaw plugin system): + +**Fix**: Add knip configuration: + +```json +// nemoclaw/knip.json +{ + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"], + "ignore": ["src/exports.ts"] // Intentionally exported +} +``` + +### Acceptable Duplication + +Some duplication is acceptable (test setup, type definitions): + +**Fix**: Configure jscpd to ignore: + +```json +// .jscpd.json +{ + "ignore": [ + "**/test/**", + "**/*.d.ts" + ] +} +``` + +## Best Practices + +1. **Run quality checks regularly** - Weekly or before each release +2. **Set quality gates** - Enforce maximum complexity and duplication +3. **Prioritize tech debt** - Address FIXMEs before TODOs +4. **Refactor incrementally** - Improve quality with each change +5. **Track trends** - Monitor quality metrics over time + +## Example Workflow + +```bash +# 1. Run full quality analysis +make complexity +make dead-code +make duplicates +make tech-debt + +# 2. Review results and prioritize fixes +# - High complexity functions? → Refactor +# - Unused code? → Delete +# - Duplication? → Extract common logic +# - Old TODOs? → Complete or create issues + +# 3. Make improvements +# ... refactor code ... + +# 4. Verify improvements +make complexity # Should show lower scores +make dead-code # Should show fewer unused items + +# 5. Commit improvements +git add -A +git commit -m "refactor: reduce complexity and remove dead code" +``` + +## Success Criteria + +✅ All functions have complexity ≤15 +✅ No unused exports or files (or justified) +✅ Code duplication <5% +✅ TODOs are tracked with issue numbers +✅ Tech debt decreases over time + +When all criteria are met, your codebase is maintainable and healthy! 🎉 + +## Related Commands + +- `make check` - Run linting and formatting checks +- `npm test` - Run test suite +- `make lint` - Run linters only diff --git a/.factory/skills/generate-release-notes/SKILL.md b/.factory/skills/generate-release-notes/SKILL.md new file mode 100644 index 000000000..68f56a36a --- /dev/null +++ b/.factory/skills/generate-release-notes/SKILL.md @@ -0,0 +1,429 @@ +--- +name: generate-release-notes +description: Generate changelog and release notes from git commits. Use when preparing releases, updating CHANGELOG.md, or documenting changes. Trigger keywords - changelog, release notes, release, version, what changed. +--- + +# Generate Release Notes + +Generate release notes and changelog from git commit history using conventional commit conventions. + +## Prerequisites + +- You must be in the NemoClaw repository root +- Git repository must have commit history +- Node.js 20+ must be installed +- Dependencies must be installed (`npm install`) + +## When to Use + +- **Before creating a release** - Document what's changed +- **Updating CHANGELOG.md** - Keep changelog current +- **Release announcements** - Generate user-facing release notes +- **Version planning** - See what features are ready to ship +- **Contributor attribution** - Acknowledge all contributors + +## Quick Commands + +### Generate Full Changelog + +```bash +# Generate complete changelog from all commits +npm run changelog:full +``` + +Creates/updates `CHANGELOG.md` with all releases. + +### Generate Latest Release Notes + +```bash +# Generate notes for latest changes +npm run changelog +``` + +Generates notes from last tag to HEAD (or all commits if no tags). + +### Manual Generation with Script + +```bash +# Generate release notes for specific version +node scripts/generate-changelog.js --version v0.2.0 + +# Generate from specific tag range +node scripts/generate-changelog.js --from v0.1.0 --to HEAD +``` + +## Commit Message Conventions + +NemoClaw uses **Conventional Commits** for automated changelog generation: + +### Format + +``` +(): + + + +