Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1c4e93c
centralize runtime detection and installer fallbacks
kjw3 Mar 18, 2026
13b38d4
fail fast on podman for macos
kjw3 Mar 18, 2026
999782d
tighten local inference setup paths
kjw3 Mar 18, 2026
3917d77
add macos install smoke test
kjw3 Mar 18, 2026
6f92270
fix smoke macos install cleanup trap
kjw3 Mar 18, 2026
8c9af49
add runtime selection to macos smoke test
kjw3 Mar 18, 2026
3976998
improve colima coredns upstream detection
kjw3 Mar 18, 2026
f6cb4fd
avoid consuming stdin in colima dns probe
kjw3 Mar 18, 2026
b0ce85d
align smoke answers with preset prompt
kjw3 Mar 18, 2026
d942624
close stdin for onboarding child commands
kjw3 Mar 18, 2026
d168e62
stage smoke answers through a pipe
kjw3 Mar 18, 2026
4b4dfbc
avoid smoke log hangs from inherited stdout
kjw3 Mar 18, 2026
cd369b6
allow prompt-driven installs to exit under piped stdin
kjw3 Mar 18, 2026
a350085
silence smoke answer feeder shutdown
kjw3 Mar 18, 2026
ba42467
tighten macos runtime selection and dns patching
kjw3 Mar 18, 2026
98a727b
clarify apple silicon macos support
kjw3 Mar 18, 2026
cef43b1
make curl installer self-contained
kjw3 Mar 18, 2026
7aca0b8
fix macos installer preflight test on linux ci
kjw3 Mar 18, 2026
29ace38
clarify runtime prerequisite wording
kjw3 Mar 18, 2026
8b83cf2
preserve interactive prompts after first answer
kjw3 Mar 18, 2026
62a68b7
remove openshell gateway volume on uninstall
kjw3 Mar 18, 2026
9428857
restore tty for sandbox connect
kjw3 Mar 18, 2026
9138059
sync sandbox inference config from onboard selection
kjw3 Mar 18, 2026
2893683
sync sandbox config via connect stdin
kjw3 Mar 18, 2026
37cbbba
register ollama models with explicit provider ids
kjw3 Mar 18, 2026
fb901cd
add stable nemoclaw shim for nvm installs
kjw3 Mar 18, 2026
0c1af98
register ollama provider config in sandbox
kjw3 Mar 18, 2026
a83bb99
route sandbox inference through managed provider
kjw3 Mar 18, 2026
320fad0
fix sandbox provider config serialization
kjw3 Mar 18, 2026
415b9e8
probe container reachability for local inference
kjw3 Mar 18, 2026
7bffd0f
require explicit local provider selection
kjw3 Mar 18, 2026
7d912cb
align provider identity across managed inference UI
kjw3 Mar 18, 2026
962c204
make validated ollama path first-class
kjw3 Mar 18, 2026
aa1eefe
add curated cloud model selector
kjw3 Mar 18, 2026
20a0e52
add ollama model selector
kjw3 Mar 18, 2026
80086b4
warm selected ollama model and quiet sandbox sync output
kjw3 Mar 18, 2026
dedbae9
fail onboarding when selected ollama model is unhealthy
kjw3 Mar 18, 2026
efe9678
fix: respect non-interactive mode in Ollama model selection
ericksoa Mar 18, 2026
ae59283
merge: resolve setup.sh conflict between runtime check and sandbox name
ericksoa Mar 18, 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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,18 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
| Linux | Ubuntu 22.04 LTS or later |
| Node.js | 20 or later |
| npm | 10 or later |
| Docker | Installed and running |
| Container runtime | Supported runtime installed and running |
| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed |

#### Container Runtime Support

| Platform | Supported runtimes | Notes |
|----------|--------------------|-------|
| Linux | Docker | Primary supported path today |
| macOS (Apple Silicon) | Colima, Docker Desktop | Recommended runtimes for supported macOS setups |
| macOS | Podman | Not supported yet. NemoClaw currently depends on OpenShell support for Podman on macOS. |
| Windows WSL | Docker Desktop (WSL backend) | Supported target path |

### Install NemoClaw and Onboard OpenClaw Agent

Download and run the installer script.
Expand Down Expand Up @@ -144,6 +153,8 @@ Inference requests from the agent never leave the sandbox directly. OpenShell in

Get an API key from [build.nvidia.com](https://build.nvidia.com). The `nemoclaw onboard` command prompts for this key during setup.

Local inference options such as Ollama and vLLM are still experimental. On macOS, they also depend on OpenShell host-routing support in addition to the local service itself being reachable on the host.

Comment on lines +156 to +157
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify local-inference status: Ollama vs vLLM.

This line says both Ollama and vLLM are experimental, but the PR objective indicates Ollama was promoted out of experimental mode. Please update wording so users aren’t misled.

📝 Suggested wording
-Local inference options such as Ollama and vLLM are still experimental. On macOS, they also depend on OpenShell host-routing support in addition to the local service itself being reachable on the host.
+vLLM local inference remains experimental. Ollama local inference is supported when host routing/reachability requirements are met. On macOS, local inference depends on OpenShell host-routing support and host service reachability.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Local inference options such as Ollama and vLLM are still experimental. On macOS, they also depend on OpenShell host-routing support in addition to the local service itself being reachable on the host.
vLLM local inference remains experimental. Ollama local inference is supported when host routing/reachability requirements are met. On macOS, local inference depends on OpenShell host-routing support and host service reachability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 144 - 145, The README sentence that groups "Ollama
and vLLM" as both experimental is inaccurate; update the phrasing in the Local
inference section so it explicitly states Ollama is no longer experimental while
vLLM remains experimental and note the macOS requirement for OpenShell only
applies when host-routing is needed; edit the sentence mentioning "Local
inference options such as Ollama and vLLM are still experimental" to separately
call out "Ollama (stable/non-experimental) and vLLM (experimental)" and keep the
OpenShell host-routing note as-is.

---

## Protection Layers
Expand Down
8 changes: 8 additions & 0 deletions bin/lib/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ 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());
});
});
Expand Down
75 changes: 75 additions & 0 deletions bin/lib/inference-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const INFERENCE_ROUTE_URL = "https://inference.local/v1";
const DEFAULT_CLOUD_MODEL = "nvidia/nemotron-3-super-120b-a12b";
const CLOUD_MODEL_OPTIONS = [
{ id: "nvidia/nemotron-3-super-120b-a12b", label: "Nemotron 3 Super 120B" },
{ id: "moonshotai/kimi-k2.5", label: "Kimi K2.5" },
{ id: "z-ai/glm5", label: "GLM-5" },
{ id: "minimaxai/minimax-m2.5", label: "MiniMax M2.5" },
{ id: "qwen/qwen3.5-397b-a17b", label: "Qwen3.5 397B A17B" },
{ id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" },
];
const DEFAULT_ROUTE_PROFILE = "inference-local";
const DEFAULT_ROUTE_CREDENTIAL_ENV = "OPENAI_API_KEY";
const MANAGED_PROVIDER_ID = "inference";
const { DEFAULT_OLLAMA_MODEL } = require("./local-inference");

function getProviderSelectionConfig(provider, model) {
switch (provider) {
case "nvidia-nim":
return {
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
ncpPartner: null,
model: model || DEFAULT_CLOUD_MODEL,
profile: DEFAULT_ROUTE_PROFILE,
credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV,
provider,
providerLabel: "NVIDIA Cloud API",
};
case "vllm-local":
return {
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
ncpPartner: null,
model: model || "vllm-local",
profile: DEFAULT_ROUTE_PROFILE,
Comment on lines +32 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the primary-model helper aligned with the provider defaults.

getProviderSelectionConfig("vllm-local") defaults to "vllm-local", but getOpenClawPrimaryModel("vllm-local") falls back to the cloud model when model is omitted. That silently misidentifies a local vLLM selection as cloud and reintroduces the implicit fallback this module is supposed to remove.

Suggested fix
 function getOpenClawPrimaryModel(provider, model) {
-  const resolvedModel =
-    model || (provider === "ollama-local" ? DEFAULT_OLLAMA_MODEL : DEFAULT_CLOUD_MODEL);
-  return resolvedModel ? `${MANAGED_PROVIDER_ID}/${resolvedModel}` : null;
+  let resolvedModel = model;
+  if (!resolvedModel) {
+    switch (provider) {
+      case "nvidia-nim":
+        resolvedModel = DEFAULT_CLOUD_MODEL;
+        break;
+      case "vllm-local":
+        resolvedModel = "vllm-local";
+        break;
+      case "ollama-local":
+        resolvedModel = DEFAULT_OLLAMA_MODEL;
+        break;
+      default:
+        return null;
+    }
+  }
+
+  return `${MANAGED_PROVIDER_ID}/${resolvedModel}`;
 }

Also applies to: 59-62

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

In `@bin/lib/inference-config.js` around lines 32 - 38, The helper
getOpenClawPrimaryModel currently falls back to the cloud model when model is
undefined for the "vllm-local" provider, causing a local selection to be treated
as cloud; update the logic in the vllm-local case in getOpenClawPrimaryModel so
that when model is falsy it returns "vllm-local" (matching
getProviderSelectionConfig) instead of the cloud default, and apply the same
change to the other similar case block (lines corresponding to the second
vllm-local branch) so both branches use "vllm-local" as the default model when
model is omitted.

credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV,
provider,
providerLabel: "Local vLLM",
};
case "ollama-local":
return {
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
ncpPartner: null,
model: model || DEFAULT_OLLAMA_MODEL,
profile: DEFAULT_ROUTE_PROFILE,
credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV,
provider,
providerLabel: "Local Ollama",
};
default:
return null;
}
}

function getOpenClawPrimaryModel(provider, model) {
const resolvedModel =
model || (provider === "ollama-local" ? DEFAULT_OLLAMA_MODEL : DEFAULT_CLOUD_MODEL);
return resolvedModel ? `${MANAGED_PROVIDER_ID}/${resolvedModel}` : null;
}

module.exports = {
CLOUD_MODEL_OPTIONS,
DEFAULT_CLOUD_MODEL,
DEFAULT_OLLAMA_MODEL,
DEFAULT_ROUTE_CREDENTIAL_ENV,
DEFAULT_ROUTE_PROFILE,
INFERENCE_ROUTE_URL,
MANAGED_PROVIDER_ID,
getOpenClawPrimaryModel,
getProviderSelectionConfig,
};
179 changes: 179 additions & 0 deletions bin/lib/local-inference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const HOST_GATEWAY_URL = "http://host.openshell.internal";
const CONTAINER_REACHABILITY_IMAGE = "curlimages/curl:8.10.1";
const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:30b";

function getLocalProviderBaseUrl(provider) {
switch (provider) {
case "vllm-local":
return `${HOST_GATEWAY_URL}:8000/v1`;
case "ollama-local":
return `${HOST_GATEWAY_URL}:11434/v1`;
default:
return null;
}
}

function getLocalProviderHealthCheck(provider) {
switch (provider) {
case "vllm-local":
return "curl -sf http://localhost:8000/v1/models 2>/dev/null";
case "ollama-local":
return "curl -sf http://localhost:11434/api/tags 2>/dev/null";
default:
return null;
}
}

function getLocalProviderContainerReachabilityCheck(provider) {
switch (provider) {
case "vllm-local":
return `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:8000/v1/models 2>/dev/null`;
case "ollama-local":
return `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:11434/api/tags 2>/dev/null`;
default:
return null;
}
}

function validateLocalProvider(provider, runCapture) {
const command = getLocalProviderHealthCheck(provider);
if (!command) {
return { ok: true };
}
Comment on lines +42 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fail unknown local provider IDs instead of accepting them.

If getLocalProviderHealthCheck() has no match, validateLocalProvider() currently returns { ok: true }. A typo such as ollama vs ollama-local will pass validation here and only fail later when the base URL/config lookup returns null.

💡 Proposed fix
 function validateLocalProvider(provider, runCapture) {
   const command = getLocalProviderHealthCheck(provider);
   if (!command) {
-    return { ok: true };
+    return {
+      ok: false,
+      message: `Unsupported local inference provider: ${provider}.`,
+    };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const command = getLocalProviderHealthCheck(provider);
if (!command) {
return { ok: true };
}
const command = getLocalProviderHealthCheck(provider);
if (!command) {
return {
ok: false,
message: `Unsupported local inference provider: ${provider}.`,
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/local-inference.js` around lines 42 - 45, validateLocalProvider
currently treats a missing health-check command from
getLocalProviderHealthCheck(provider) as success; change it to fail fast for
unknown local provider IDs by returning an error result (e.g., { ok: false,
error: 'Unknown local provider id: <provider>' }) when
getLocalProviderHealthCheck(provider) returns falsy. Update the logic in
validateLocalProvider (and any callers expecting the previous shape) so typos
like "ollama" vs "ollama-local" are rejected immediately instead of later when
config/base URL lookups occur.


const output = runCapture(command, { ignoreError: true });
if (!output) {
switch (provider) {
case "vllm-local":
return {
ok: false,
message: "Local vLLM was selected, but nothing is responding on http://localhost:8000.",
};
case "ollama-local":
return {
ok: false,
message: "Local Ollama was selected, but nothing is responding on http://localhost:11434.",
};
default:
return { ok: false, message: "The selected local inference provider is unavailable." };
}
}

const containerCommand = getLocalProviderContainerReachabilityCheck(provider);
if (!containerCommand) {
return { ok: true };
}

const containerOutput = runCapture(containerCommand, { ignoreError: true });
if (containerOutput) {
return { ok: true };
}

switch (provider) {
case "vllm-local":
return {
ok: false,
message:
"Local vLLM is responding on localhost, but containers cannot reach http://host.openshell.internal:8000. Ensure the server is reachable from containers, not only from the host shell.",
};
case "ollama-local":
return {
ok: false,
message:
"Local Ollama is responding on localhost, but containers cannot reach http://host.openshell.internal:11434. Ensure Ollama listens on 0.0.0.0:11434 instead of 127.0.0.1 so sandboxes can reach it.",
};
default:
return { ok: false, message: "The selected local inference provider is unavailable from containers." };
}
}

function parseOllamaList(output) {
return String(output || "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => !/^NAME\s+/i.test(line))
.map((line) => line.split(/\s{2,}/)[0])
.filter(Boolean);
}

function getOllamaModelOptions(runCapture) {
const output = runCapture("ollama list 2>/dev/null", { ignoreError: true });
const parsed = parseOllamaList(output);
if (parsed.length > 0) {
return parsed;
}
return [DEFAULT_OLLAMA_MODEL];
}

function getDefaultOllamaModel(runCapture) {
const models = getOllamaModelOptions(runCapture);
return models.includes(DEFAULT_OLLAMA_MODEL) ? DEFAULT_OLLAMA_MODEL : models[0];
}

function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

function getOllamaWarmupCommand(model, keepAlive = "15m") {
const payload = JSON.stringify({
model,
prompt: "hello",
stream: false,
keep_alive: keepAlive,
});
return `nohup curl -s http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`;
}

function getOllamaProbeCommand(model, timeoutSeconds = 120, keepAlive = "15m") {
const payload = JSON.stringify({
model,
prompt: "hello",
stream: false,
keep_alive: keepAlive,
});
return `curl -sS --max-time ${timeoutSeconds} http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} 2>/dev/null`;
}

function validateOllamaModel(model, runCapture) {
const output = runCapture(getOllamaProbeCommand(model), { ignoreError: true });
if (!output) {
return {
ok: false,
message:
`Selected Ollama model '${model}' did not answer the local probe in time. ` +
"It may still be loading, too large for the host, or otherwise unhealthy.",
};
}

try {
const parsed = JSON.parse(output);
if (parsed && typeof parsed.error === "string" && parsed.error.trim()) {
return {
ok: false,
message: `Selected Ollama model '${model}' failed the local probe: ${parsed.error.trim()}`,
};
}
} catch {}

return { ok: true };
}

module.exports = {
CONTAINER_REACHABILITY_IMAGE,
DEFAULT_OLLAMA_MODEL,
HOST_GATEWAY_URL,
getDefaultOllamaModel,
getLocalProviderBaseUrl,
getLocalProviderContainerReachabilityCheck,
getLocalProviderHealthCheck,
getOllamaModelOptions,
getOllamaProbeCommand,
getOllamaWarmupCommand,
parseOllamaList,
validateOllamaModel,
validateLocalProvider,
};
Loading
Loading