Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b
ARG NEMOCLAW_PROVIDER_KEY=nvidia
ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b
ARG CHAT_UI_URL=http://127.0.0.1:18789
ARG NEMOCLAW_INSECURE_LOCAL_UI=0
ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1
ARG NEMOCLAW_INFERENCE_API=openai-completions
ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=
Expand All @@ -65,6 +66,7 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_PROVIDER_KEY=${NEMOCLAW_PROVIDER_KEY} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
CHAT_UI_URL=${CHAT_UI_URL} \
NEMOCLAW_INSECURE_LOCAL_UI=${NEMOCLAW_INSECURE_LOCAL_UI} \
NEMOCLAW_INFERENCE_BASE_URL=${NEMOCLAW_INFERENCE_BASE_URL} \
NEMOCLAW_INFERENCE_API=${NEMOCLAW_INFERENCE_API} \
NEMOCLAW_INFERENCE_COMPAT_B64=${NEMOCLAW_INFERENCE_COMPAT_B64}
Expand All @@ -87,10 +89,15 @@ primary_model_ref = os.environ['NEMOCLAW_PRIMARY_MODEL_REF']; \
inference_base_url = os.environ['NEMOCLAW_INFERENCE_BASE_URL']; \
inference_api = os.environ['NEMOCLAW_INFERENCE_API']; \
inference_compat = json.loads(base64.b64decode(os.environ['NEMOCLAW_INFERENCE_COMPAT_B64']).decode('utf-8')); \
insecure_local_ui = os.environ.get('NEMOCLAW_INSECURE_LOCAL_UI', '0').strip().lower() in ('1', 'true', 'yes', 'on'); \
parsed = urlparse(chat_ui_url); \
chat_origin = f'{parsed.scheme}://{parsed.netloc}' if parsed.scheme and parsed.netloc else 'http://127.0.0.1:18789'; \
origins = ['http://127.0.0.1:18789']; \
origins = list(dict.fromkeys(origins + [chat_origin])); \
loopback_hosts = {'127.0.0.1', 'localhost', '::1'}; \
origin_hosts = {urlparse(origin).hostname for origin in origins}; \
loopback_only_origins = bool(origin_hosts) and all(host and host in loopback_hosts for host in origin_hosts); \
enable_insecure_local_ui = insecure_local_ui and loopback_only_origins; \
providers = { \
provider_key: { \
'baseUrl': inference_base_url, \
Expand All @@ -106,8 +113,8 @@ config = { \
'gateway': { \
'mode': 'local', \
'controlUi': { \
'allowInsecureAuth': True, \
'dangerouslyDisableDeviceAuth': True, \
'allowInsecureAuth': enable_insecure_local_ui, \
'dangerouslyDisableDeviceAuth': enable_insecure_local_ui, \
'allowedOrigins': origins, \
}, \
'trustedProxies': ['127.0.0.1', '::1'], \
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,21 @@ The script installs Node.js if it is not already present, then runs the guided o
>
> NemoClaw creates a fresh OpenClaw instance inside the sandbox during the onboarding process.

For the safer default, NemoClaw keeps OpenClaw device auth enabled for the Control UI.
If you explicitly want token-only auth for a loopback-only local dashboard on first run, use:

```bash
curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_INSECURE_LOCAL_UI=1 bash
```

Otherwise, use the standard install path:

```bash
curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

You can also `export NEMOCLAW_INSECURE_LOCAL_UI=1` before running the installer if you prefer a two-step flow.

If you use nvm or fnm to manage Node.js, the installer may not update your current shell's PATH.
If `nemoclaw` is not found after install, run `source ~/.bashrc` (or `source ~/.zshrc` for zsh) or open a new terminal.

Expand Down
9 changes: 9 additions & 0 deletions bin/lib/onboard-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ function defaultSteps() {
};
}

function optionalBoolean(value) {
return typeof value === "boolean" ? value : null;
}

function createSession(overrides = {}) {
const now = new Date().toISOString();
const insecureLocalUi = optionalBoolean(overrides.insecureLocalUi);
return {
version: SESSION_VERSION,
sessionId: overrides.sessionId || `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
Expand All @@ -54,6 +59,7 @@ function createSession(overrides = {}) {
credentialEnv: overrides.credentialEnv || null,
preferredInferenceApi: overrides.preferredInferenceApi || null,
nimContainer: overrides.nimContainer || null,
insecureLocalUi,
policyPresets: Array.isArray(overrides.policyPresets)
? overrides.policyPresets.filter((value) => typeof value === "string")
: null,
Expand Down Expand Up @@ -136,6 +142,7 @@ function normalizeSession(data) {
preferredInferenceApi:
typeof data.preferredInferenceApi === "string" ? data.preferredInferenceApi : null,
nimContainer: typeof data.nimContainer === "string" ? data.nimContainer : null,
insecureLocalUi: optionalBoolean(data.insecureLocalUi),
policyPresets: Array.isArray(data.policyPresets)
? data.policyPresets.filter((value) => typeof value === "string")
: null,
Expand Down Expand Up @@ -370,6 +377,7 @@ function filterSafeUpdates(updates) {
if (typeof updates.preferredInferenceApi === "string")
safe.preferredInferenceApi = updates.preferredInferenceApi;
if (typeof updates.nimContainer === "string") safe.nimContainer = updates.nimContainer;
if (typeof updates.insecureLocalUi === "boolean") safe.insecureLocalUi = updates.insecureLocalUi;
if (Array.isArray(updates.policyPresets)) {
safe.policyPresets = updates.policyPresets.filter((value) => typeof value === "string");
}
Expand Down Expand Up @@ -398,6 +406,7 @@ function summarizeForDebug(session = loadSession()) {
credentialEnv: session.credentialEnv,
preferredInferenceApi: session.preferredInferenceApi,
nimContainer: session.nimContainer,
insecureLocalUi: session.insecureLocalUi,
policyPresets: session.policyPresets,
lastStepStarted: session.lastStepStarted,
lastCompletedStep: session.lastCompletedStep,
Expand Down
123 changes: 100 additions & 23 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,9 +1020,11 @@ function patchStagedDockerfile(
buildId = String(Date.now()),
provider = null,
preferredInferenceApi = null,
options = {},
) {
const { providerKey, primaryModelRef, inferenceBaseUrl, inferenceApi, inferenceCompat } =
getSandboxInferenceConfig(model, provider, preferredInferenceApi);
const insecureLocalUiValue = options.insecureLocalUi === true ? "1" : "0";
let dockerfile = fs.readFileSync(dockerfilePath, "utf8");
dockerfile = dockerfile.replace(/^ARG NEMOCLAW_MODEL=.*$/m, `ARG NEMOCLAW_MODEL=${model}`);
dockerfile = dockerfile.replace(
Expand All @@ -1034,6 +1036,10 @@ function patchStagedDockerfile(
`ARG NEMOCLAW_PRIMARY_MODEL_REF=${primaryModelRef}`,
);
dockerfile = dockerfile.replace(/^ARG CHAT_UI_URL=.*$/m, `ARG CHAT_UI_URL=${chatUiUrl}`);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INSECURE_LOCAL_UI=.*$/m,
`ARG NEMOCLAW_INSECURE_LOCAL_UI=${insecureLocalUiValue}`,
);
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_INFERENCE_BASE_URL=.*$/m,
`ARG NEMOCLAW_INFERENCE_BASE_URL=${inferenceBaseUrl}`,
Expand All @@ -1053,6 +1059,22 @@ function patchStagedDockerfile(
fs.writeFileSync(dockerfilePath, dockerfile);
}

function isInsecureLocalUiRequested(value = process.env.NEMOCLAW_INSECURE_LOCAL_UI || "") {
return /^(1|true|yes|on)$/i.test(value);
}

function resolveInsecureLocalUiPreference(session) {
if (typeof session?.insecureLocalUi === "boolean") {
return session.insecureLocalUi;
}
const insecureLocalUi = isInsecureLocalUiRequested();
onboardSession.updateSession((current) => {
current.insecureLocalUi = insecureLocalUi;
return current;
});
return insecureLocalUi;
}

function summarizeProbeError(body, status) {
if (!body) return `HTTP ${status} with no response body`;
try {
Expand Down Expand Up @@ -2313,6 +2335,7 @@ async function createSandbox(
provider,
preferredInferenceApi = null,
sandboxNameOverride = null,
options = {},
) {
step(5, 7, "Creating sandbox");

Expand Down Expand Up @@ -2371,13 +2394,18 @@ async function createSandbox(
// --gpu is intentionally omitted. See comment in startGateway().

console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`);
const insecureLocalUiEnabled =
typeof options.insecureLocalUi === "boolean"
? options.insecureLocalUi
: isInsecureLocalUiRequested();
patchStagedDockerfile(
stagedDockerfile,
model,
chatUiUrl,
String(Date.now()),
provider,
preferredInferenceApi,
{ insecureLocalUi: insecureLocalUiEnabled },
);
// Only pass non-sensitive env vars to the sandbox. NVIDIA_API_KEY is NOT
// needed inside the sandbox — inference is proxied through the OpenShell
Expand Down Expand Up @@ -3680,6 +3708,64 @@ function startRecordedStep(stepName, updates = {}) {
}
}

async function ensureOnboardSandbox({
gpu,
model,
provider,
preferredInferenceApi,
sandboxName,
nimContainer,
insecureLocalUi,
resume,
session,
}) {
const sandboxReuseState = getSandboxReuseState(sandboxName);
const resumeSandbox =
resume && session?.steps?.sandbox?.status === "complete" && sandboxReuseState === "ready";
if (resumeSandbox) {
skippedStepMessage("sandbox", sandboxName);
return { sandboxName, insecureLocalUi };
}

if (resume && session?.steps?.sandbox?.status === "complete") {
if (sandboxReuseState === "not_ready") {
note(` [resume] Recorded sandbox '${sandboxName}' exists but is not ready; recreating it.`);
repairRecordedSandbox(sandboxName);
} else {
note(" [resume] Recorded sandbox state is unavailable; recreating it.");
if (sandboxName) {
registry.removeSandbox(sandboxName);
}
}
}

startRecordedStep("sandbox", { sandboxName, provider, model });
const resolvedInsecureLocalUi =
typeof insecureLocalUi === "boolean"
? insecureLocalUi
: resolveInsecureLocalUiPreference(session);
const createdSandboxName = await createSandbox(
gpu,
model,
provider,
preferredInferenceApi,
sandboxName,
{ insecureLocalUi: resolvedInsecureLocalUi },
);
onboardSession.markStepComplete("sandbox", {
sandboxName: createdSandboxName,
provider,
model,
nimContainer,
insecureLocalUi: resolvedInsecureLocalUi,
});

return {
sandboxName: createdSandboxName,
insecureLocalUi: resolvedInsecureLocalUi,
};
}

const ONBOARD_STEP_INDEX = {
preflight: { number: 1, title: "Preflight checks" },
gateway: { number: 2, title: "Starting OpenShell gateway" },
Expand Down Expand Up @@ -3840,6 +3926,8 @@ async function onboard(opts = {}) {
let credentialEnv = session?.credentialEnv || null;
let preferredInferenceApi = session?.preferredInferenceApi || null;
let nimContainer = session?.nimContainer || null;
let insecureLocalUi =
typeof session?.insecureLocalUi === "boolean" ? session.insecureLocalUi : null;
let forceProviderSelection = false;
while (true) {
const resumeProviderSelection =
Expand Down Expand Up @@ -3912,29 +4000,17 @@ async function onboard(opts = {}) {
break;
}

const sandboxReuseState = getSandboxReuseState(sandboxName);
const resumeSandbox =
resume && session?.steps?.sandbox?.status === "complete" && sandboxReuseState === "ready";
if (resumeSandbox) {
skippedStepMessage("sandbox", sandboxName);
} else {
if (resume && session?.steps?.sandbox?.status === "complete") {
if (sandboxReuseState === "not_ready") {
note(
` [resume] Recorded sandbox '${sandboxName}' exists but is not ready; recreating it.`,
);
repairRecordedSandbox(sandboxName);
} else {
note(" [resume] Recorded sandbox state is unavailable; recreating it.");
if (sandboxName) {
registry.removeSandbox(sandboxName);
}
}
}
startRecordedStep("sandbox", { sandboxName, provider, model });
sandboxName = await createSandbox(gpu, model, provider, preferredInferenceApi, sandboxName);
onboardSession.markStepComplete("sandbox", { sandboxName, provider, model, nimContainer });
}
({ sandboxName, insecureLocalUi } = await ensureOnboardSandbox({
gpu,
model,
provider,
preferredInferenceApi,
sandboxName,
nimContainer,
insecureLocalUi,
resume,
session,
}));

const resumeOpenclaw = resume && sandboxName && isOpenclawReady(sandboxName);
if (resumeOpenclaw) {
Expand Down Expand Up @@ -4026,6 +4102,7 @@ module.exports = {
onboardSession,
printSandboxCreateRecoveryHints,
pruneStaleSandboxEntry,
resolveInsecureLocalUiPreference,
repairRecordedSandbox,
recoverGatewayRuntime,
resolveDashboardForwardTarget,
Expand Down
1 change: 1 addition & 0 deletions docs/reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,6 @@ The following environment variables configure optional services and local access
| `TELEGRAM_BOT_TOKEN` | Bot token for the Telegram bridge. |
| `ALLOWED_CHAT_IDS` | Comma-separated list of Telegram chat IDs allowed to message the agent. |
| `CHAT_UI_URL` | URL for the optional chat UI endpoint. |
| `NEMOCLAW_INSECURE_LOCAL_UI` | Set to `1` to allow token-only Control UI auth for loopback-only dashboard origins. |

For normal setup and reconfiguration, prefer `nemoclaw onboard` over editing these files by hand.
5 changes: 5 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ usage() {
printf " ${C_DIM}Usage:${C_RESET}\n"
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash\n"
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -s -- [options]\n\n"
printf " ${C_DIM}Examples:${C_RESET}\n"
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_INSECURE_LOCAL_UI=1 bash\n"
printf " export NEMOCLAW_INSECURE_LOCAL_UI=1\n"
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash\n\n"
printf " ${C_DIM}Options:${C_RESET}\n"
printf " --non-interactive Skip prompts (uses env vars / defaults)\n"
printf " --version, -v Print installer version and exit\n"
Expand All @@ -222,6 +226,7 @@ usage() {
printf " NEMOCLAW_POLICY_PRESETS Comma-separated policy presets\n"
printf " NEMOCLAW_EXPERIMENTAL=1 Show experimental/local options\n"
printf " CHAT_UI_URL Chat UI URL to open after setup\n"
printf " NEMOCLAW_INSECURE_LOCAL_UI=1 Allow token-only local UI auth for loopback origins\n"
printf " DISCORD_BOT_TOKEN Auto-enable Discord policy support\n"
printf " SLACK_BOT_TOKEN Auto-enable Slack policy support\n"
printf " TELEGRAM_BOT_TOKEN Auto-enable Telegram policy support\n"
Expand Down
31 changes: 28 additions & 3 deletions k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

> **⚠️ Experimental**: This deployment method is intended for **trying out NemoClaw on Kubernetes**, not for production use. It requires a **privileged pod** running **Docker-in-Docker (DinD)** to create isolated sandbox environments. Operational requirements (storage, runtime, security policies) vary by cluster configuration.

The sample manifest now uses a few safer defaults out of the box:
- disables Kubernetes service account token automounting
- disables service-link environment injection
- runs the workspace container with `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, and `RuntimeDefault` seccomp
- applies NemoClaw's suggested policy presets instead of skipping policy setup
- downloads the installer to a local file with HTTPS-only curl flags before execution

Run [NemoClaw](https://github.com/NVIDIA/NemoClaw) on Kubernetes with GPU inference powered by [Dynamo](https://github.com/ai-dynamo/dynamo) or any OpenAI-compatible endpoint.

---
Expand All @@ -17,8 +24,16 @@ Run [NemoClaw](https://github.com/NVIDIA/NemoClaw) on Kubernetes with GPU infere

### 1. Deploy NemoClaw

If your compatible endpoint requires an API key, create the optional
`nemoclaw-compatible-api-key` Secret after creating the namespace and before
running `kubectl apply`. The same Secret-backed flow is described again in the
configuration section below.

```bash
kubectl create namespace nemoclaw
kubectl create secret generic nemoclaw-compatible-api-key \
-n nemoclaw \
--from-literal=api-key='<your-api-key>'
kubectl apply -f https://raw.githubusercontent.com/NVIDIA/NemoClaw/main/k8s/nemoclaw-k8s.yaml
```

Expand Down Expand Up @@ -48,9 +63,21 @@ Edit the environment variables in `nemoclaw-k8s.yaml` before deploying:
|----------|----------|-------------|
| `DYNAMO_HOST` | Yes | Inference endpoint for socat proxy (e.g., `vllm-frontend.dynamo.svc:8000`) |
| `NEMOCLAW_ENDPOINT_URL` | Yes | URL the sandbox uses (usually `http://host.openshell.internal:8000/v1`) |
| `COMPATIBLE_API_KEY` | Yes | API key (use `dummy` for Dynamo/vLLM) |
| `COMPATIBLE_API_KEY` | No | Loaded from the optional `nemoclaw-compatible-api-key` Secret; defaults to `dummy` for Dynamo/vLLM when the Secret is absent |
| `NEMOCLAW_MODEL` | Yes | Model name (e.g., `meta-llama/Llama-3.1-8B-Instruct`) |
| `NEMOCLAW_SANDBOX_NAME` | No | Sandbox name (default: `my-assistant`) |
| `NEMOCLAW_POLICY_MODE` | No | Policy preset mode for non-interactive onboarding (default: `suggested`) |

### Optional: Store a Real API Key in a Secret

If your compatible endpoint requires authentication, create the Secret before
you apply the manifest in Step 1:

```bash
kubectl create secret generic nemoclaw-compatible-api-key \
-n nemoclaw \
--from-literal=api-key='<your-api-key>'
```

### Example: Custom Endpoint

Expand All @@ -60,8 +87,6 @@ env:
value: "my-vllm.my-namespace.svc.cluster.local:8000"
- name: NEMOCLAW_ENDPOINT_URL
value: "http://host.openshell.internal:8000/v1"
- name: COMPATIBLE_API_KEY
value: "dummy"
- name: NEMOCLAW_MODEL
value: "mistralai/Mistral-7B-Instruct-v0.3"
```
Expand Down
Loading