Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
902d839
feat: add Jetson Orin Nano support
realkim93 Mar 19, 2026
674493e
fix: address code review — null-safety, vllm-local parity, policy tig…
realkim93 Mar 20, 2026
9601a71
fix: address review feedback — port cleanup timing, provider mapping,…
realkim93 Mar 23, 2026
8e23931
fix: remove port-18789 preflight check to avoid regression on re-run
realkim93 Mar 23, 2026
9d57586
fix: align test assertions with merged implementation
realkim93 Mar 23, 2026
59291f1
fix: align tests with main after rebase
realkim93 Mar 29, 2026
92e51f1
refactor: extract Jetson detection to reduce detectGpu complexity
realkim93 Mar 29, 2026
c4d41dd
chore: apply shfmt formatting to setup-jetson.sh
realkim93 Mar 29, 2026
2fad75c
Merge branch 'main' into feat/jetson-orin-nano-support
cv Mar 30, 2026
79af0ff
fix: restore preflight idempotency and fix local provider sandbox config
realkim93 Mar 30, 2026
df08f88
merge: resolve conflicts with latest main
realkim93 Apr 1, 2026
fc8c790
fix: correct setup-jetson placement and apply Prettier formatting
realkim93 Apr 1, 2026
73ca60d
Merge remote-tracking branch 'origin/main' into pr-405-restore
realkim93 Apr 1, 2026
2115aa4
Merge branch 'main' into feat/jetson-orin-nano-support
realkim93 Apr 1, 2026
7b948ee
fix: address review feedback before re-review request
realkim93 Apr 1, 2026
e9bfa88
test: replace source-text inspection with behavioral tests for patchG…
realkim93 Apr 1, 2026
7b31aa5
test: add preflight gateway-reuse idempotency tests and setup-jetson …
realkim93 Apr 1, 2026
4542d9f
Merge branch 'main' into feat/jetson-orin-nano-support
realkim93 Apr 2, 2026
7a80c8d
docs: add Jetson to quickstart compatibility table, remove fragile test
realkim93 Apr 2, 2026
b7536f1
Merge branch 'feat/jetson-orin-nano-support' of github.com:realkim93/…
realkim93 Apr 2, 2026
17b7e81
merge: add new TS source files and Jetson support from merge with main
realkim93 Apr 2, 2026
fec9cec
merge: resolve conflicts with main's CJS→TS migration
realkim93 Apr 2, 2026
148c01d
merge: resolve conflict with main's stale gateway comment update
realkim93 Apr 3, 2026
08b2481
merge: resolve conflicts with latest main (onboard.js + SKILL.md)
realkim93 Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 102 additions & 2 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,12 @@ function getSandboxInferenceConfig(model, provider = null, preferredInferenceApi
supportsStore: false,
};
break;
case "ollama-local":
case "vllm-local":
providerKey = "inference";
primaryModelRef = `inference/${model}`;
inferenceBaseUrl = getLocalProviderBaseUrl(provider);
break;
case "nvidia-prod":
case "nvidia-nim":
default:
Expand Down Expand Up @@ -1829,7 +1835,10 @@ async function preflight() {

// GPU
const gpu = nim.detectGpu();
if (gpu && gpu.type === "nvidia") {
if (gpu && gpu.type === "nvidia" && gpu.jetson) {
console.log(` ✓ NVIDIA Jetson detected: ${gpu.name}, ${gpu.totalMemoryMB} MB unified memory`);
console.log(" ⓘ NIM containers not supported on Jetson — will use Ollama or cloud inference");
} else if (gpu && gpu.type === "nvidia") {
console.log(` ✓ NVIDIA GPU detected: ${gpu.count} GPU(s), ${gpu.totalMemoryMB} MB VRAM`);
if (!gpu.nimCapable) {
console.log(" ⓘ GPU VRAM too small for local NIM — will use cloud inference");
Expand Down Expand Up @@ -1889,9 +1898,85 @@ async function preflight() {
return gpu;
}

// ── Jetson gateway image patch ───────────────────────────────────
//
// JetPack kernels (Tegra) ship without nft_chain_filter and related
// nf_tables modules. The OpenShell gateway image embeds k3s, whose
// network policy controller calls iptables in nf_tables mode by default.
// Without kernel support the controller panics on startup.
//
// This function rebuilds the gateway image locally, switching the
// default iptables alternative to iptables-legacy so all rule
// manipulation uses the classic xtables backend that Tegra kernels
// fully support.

/** Extracts the semver tag from the installed openshell CLI version. */
function getGatewayImageTag() {
const openshellVersion =
runCapture("openshell --version 2>/dev/null", { ignoreError: true }) || "";
const match = openshellVersion.match(/(\d+\.\d+\.\d+)/);
return match ? match[1] : "latest";
}

/**
* Rebuilds the OpenShell gateway container image with iptables-legacy as the
* default backend. Idempotent — skips rebuild if the image is already patched
* (checked via Docker label). Required on Jetson because the Tegra kernel
* lacks nft_chain_filter modules that k3s's network policy controller needs.
*/
function patchGatewayImageForJetson() {
const tag = getGatewayImageTag();
const image = `ghcr.io/nvidia/openshell/cluster:${tag}`;

// Check if already patched (look for our label)
const inspectOut = (
runCapture(
`docker inspect --format='{{index .Config.Labels "io.nemoclaw.jetson-patched"}}' ${shellQuote(image)} 2>/dev/null`,
{ ignoreError: true },
) || ""
).trim();
if (inspectOut === "true") {
console.log(" ✓ Gateway image already patched for Jetson");
return;
}

console.log(" Patching gateway image for Jetson (iptables-legacy)...");
console.log(" (this may take a moment on first run if the base image needs to be pulled)");

const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-jetson-"));
try {
const dockerfile = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfile,
[
`FROM ${image}`,
`RUN if command -v update-alternatives >/dev/null 2>&1 && \\`,
` update-alternatives --set iptables /usr/sbin/iptables-legacy 2>/dev/null && \\`,
` update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy 2>/dev/null; then \\`,
` :; \\`,
` elif [ -f /usr/sbin/iptables-legacy ] && [ -f /usr/sbin/ip6tables-legacy ]; then \\`,
` ln -sf /usr/sbin/iptables-legacy /usr/sbin/iptables; \\`,
` ln -sf /usr/sbin/ip6tables-legacy /usr/sbin/ip6tables; \\`,
` else \\`,
` echo "iptables-legacy not available in base image" >&2; exit 1; \\`,
` fi`,
`LABEL io.nemoclaw.jetson-patched="true"`,
"",
].join("\n"),
);

run(`docker build --quiet -t ${shellQuote(image)} ${shellQuote(tmpDir)}`, {
ignoreError: false,
});
console.log(" ✓ Gateway image patched for Jetson (iptables-legacy)");
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}

// ── Step 2: Gateway ──────────────────────────────────────────────

async function startGatewayWithOptions(_gpu, { exitOnFailure = true } = {}) {
async function startGatewayWithOptions(gpu, { exitOnFailure = true } = {}) {
step(2, 7, "Starting OpenShell gateway");

const gatewayStatus = runCaptureOpenshell(["status"], { ignoreError: true });
Expand All @@ -1906,6 +1991,15 @@ async function startGatewayWithOptions(_gpu, { exitOnFailure = true } = {}) {
return;
}

// Jetson (Tegra kernel): The k3s container image ships iptables v1.8.10 in
// nf_tables mode, but JetPack kernels lack the nft_chain_filter module,
// causing the k3s network policy controller to panic on startup.
// Workaround: rebuild the gateway image locally with iptables-legacy as the
// default so iptables commands use the legacy (xtables) backend instead.
if (gpu && gpu.jetson) {
patchGatewayImageForJetson();
}

// When a stale gateway is detected (metadata exists but container is gone,
// e.g. after a Docker/Colima restart), skip the destroy — `gateway start`
// can recover the container without wiping metadata and mTLS certs.
Expand Down Expand Up @@ -2122,6 +2216,10 @@ async function createSandbox(
registry.removeSandbox(sandboxName);
}

// Kill stale dashboard-forward processes only when we are actually
// creating or recreating — avoids breaking a healthy forward on no-op reruns.
run("kill $(lsof -ti :18789 -c openclaw) 2>/dev/null || true", { ignoreError: true });

// Stage build context
const buildCtx = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-build-"));
const stagedDockerfile = path.join(buildCtx, "Dockerfile");
Expand Down Expand Up @@ -3770,6 +3868,7 @@ module.exports = {
classifySandboxCreateFailure,
createSandbox,
getFutureShellPathHint,
getGatewayImageTag,
getGatewayStartEnv,
getGatewayReuseState,
getSandboxInferenceConfig,
Expand Down Expand Up @@ -3805,6 +3904,7 @@ module.exports = {
arePolicyPresetsApplied,
setupPoliciesWithSelection,
hydrateCredentialEnv,
patchGatewayImageForJetson,
shouldIncludeBuildContextPath,
writeSandboxConfigSyncFile,
patchStagedDockerfile,
Expand Down
10 changes: 10 additions & 0 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const GLOBAL_COMMANDS = new Set([
"deploy",
"setup",
"setup-spark",
"setup-jetson",
"start",
"stop",
"status",
Expand Down Expand Up @@ -651,6 +652,11 @@ async function setupSpark() {
run(`sudo bash "${SCRIPTS}/setup-spark.sh"`);
}

async function setupJetson() {
// setup-jetson.sh configures Docker runtime + iptables-legacy for Jetson.
run(`sudo bash "${SCRIPTS}/setup-jetson.sh"`);
}

// eslint-disable-next-line complexity
async function deploy(instanceName) {
if (!instanceName) {
Expand Down Expand Up @@ -1199,6 +1205,7 @@ function help() {
${B}nemoclaw onboard${R} Configure inference endpoint and credentials
${D}(non-interactive: ${NOTICE_ACCEPT_FLAG} or ${NOTICE_ACCEPT_ENV}=1)${R}
nemoclaw setup-spark Set up on DGX Spark ${D}(fixes cgroup v2 + Docker)${R}
nemoclaw setup-jetson Set up on Jetson ${D}(Docker runtime + iptables-legacy)${R}

${G}Sandbox Management:${R}
${B}nemoclaw list${R} List all sandboxes
Expand Down Expand Up @@ -1261,6 +1268,9 @@ const [cmd, ...args] = process.argv.slice(2);
case "setup-spark":
await setupSpark();
break;
case "setup-jetson":
await setupJetson();
break;
case "deploy":
await deploy(args[0]);
break;
Expand Down
1 change: 1 addition & 0 deletions docs/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
| macOS (Intel) | Podman | Not supported yet. Depends on OpenShell support for Podman on macOS. |
| Windows WSL | Docker Desktop (WSL backend) | Supported target path. |
| DGX Spark | Docker | Refer to the [DGX Spark setup guide](https://github.com/NVIDIA/NemoClaw/blob/main/spark-install.md) for cgroup v2 and Docker configuration. |
| Jetson (Orin Nano, Orin NX, AGX Orin, Xavier) | Docker | Run `sudo nemoclaw setup-jetson` before onboarding. See [commands reference](../reference/commands.md#nemoclaw-setup-jetson). |

## Install NemoClaw and Onboard OpenClaw Agent

Expand Down
10 changes: 10 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ After the fixes complete, the script prompts you to run `nemoclaw onboard` to co
$ sudo nemoclaw setup-spark
```

### `nemoclaw setup-jetson`

Set up NemoClaw on NVIDIA Jetson devices (Orin Nano, Orin NX, AGX Orin, Xavier).
This command configures the NVIDIA container runtime for Docker and applies iptables-legacy fixes required by Jetson's Tegra kernel.
Run with `sudo` on the Jetson host.

```console
$ sudo nemoclaw setup-jetson
```

### `nemoclaw debug`

Collect diagnostics for bug reports.
Expand Down
Loading