diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index f68cce370..9cdb0ff3b 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -3221,11 +3221,19 @@ async function setupOpenclaw(sandboxName, model, provider) { // ── Step 7: Policy presets ─────────────────────────────────────── // eslint-disable-next-line complexity -async function _setupPolicies(sandboxName) { +async function _setupPolicies(sandboxName, provider = null) { step(7, 7, "Policy presets"); const suggestions = ["pypi", "npm"]; + // Auto-detect local inference — sandbox needs host gateway egress + const sandbox = registry.getSandbox(sandboxName); + const sandboxProvider = provider || (sandbox ? sandbox.provider : null); + if (sandboxProvider === "ollama-local" || sandboxProvider === "vllm-local") { + suggestions.push("local-inference"); + console.log(` Auto-detected: ${sandboxProvider} → suggesting local-inference preset`); + } + // Auto-detect based on env tokens if (getCredential("TELEGRAM_BOT_TOKEN")) { suggestions.push("telegram"); diff --git a/bin/lib/runner.js b/bin/lib/runner.js index 3b09e4fb8..f06c9bc74 100644 --- a/bin/lib/runner.js +++ b/bin/lib/runner.js @@ -8,6 +8,33 @@ const { detectDockerHost } = require("./platform"); const ROOT = path.resolve(__dirname, "..", ".."); const SCRIPTS = path.join(ROOT, "scripts"); +/** + * Redact known secret patterns from a string to prevent credential leaks + * in logs, error messages, and terminal output. + * + * Matches: + * - Environment-style assignments: NVIDIA_API_KEY=sk-... → NVIDIA_API_KEY= + * - NVIDIA API key prefix: nvapi-Abc123... → + * - GitHub PAT prefix: ghp_Abc123... → + * - Bearer tokens: Bearer eyJhb... → Bearer + */ +const SECRET_PATTERNS = [ + { re: /(NVIDIA_API_KEY|API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|_KEY)=\S+/gi, replacement: "$1=" }, + { re: /nvapi-[A-Za-z0-9_-]{10,}/g, replacement: "" }, + { re: /ghp_[A-Za-z0-9]{30,}/g, replacement: "" }, + { re: /(Bearer )\S+/gi, replacement: "$1" }, +]; + +function redactSecrets(str) { + if (typeof str !== "string") return str; + let result = str; + for (const { re, replacement } of SECRET_PATTERNS) { + re.lastIndex = 0; + result = result.replace(re, replacement); + } + return result; +} + const dockerHost = detectDockerHost(); if (dockerHost) { process.env.DOCKER_HOST = dockerHost.dockerHost; @@ -22,7 +49,7 @@ function run(cmd, opts = {}) { env: { ...process.env, ...opts.env }, }); if (result.status !== 0 && !opts.ignoreError) { - console.error(` Command failed (exit ${result.status}): ${cmd.slice(0, 80)}`); + console.error(` Command failed (exit ${result.status}): ${redactSecrets(cmd.slice(0, 80))}`); process.exit(result.status || 1); } return result; @@ -37,7 +64,7 @@ function runInteractive(cmd, opts = {}) { env: { ...process.env, ...opts.env }, }); if (result.status !== 0 && !opts.ignoreError) { - console.error(` Command failed (exit ${result.status}): ${cmd.slice(0, 80)}`); + console.error(` Command failed (exit ${result.status}): ${redactSecrets(cmd.slice(0, 80))}`); process.exit(result.status || 1); } return result; @@ -85,4 +112,4 @@ function validateName(name, label = "name") { return name; } -module.exports = { ROOT, SCRIPTS, run, runCapture, runInteractive, shellQuote, validateName }; +module.exports = { ROOT, SCRIPTS, run, runCapture, runInteractive, shellQuote, validateName, redactSecrets }; diff --git a/bin/nemoclaw.js b/bin/nemoclaw.js index d19317c16..9d899838c 100755 --- a/bin/nemoclaw.js +++ b/bin/nemoclaw.js @@ -632,8 +632,10 @@ async function setup() { } async function setupSpark() { - // setup-spark.sh configures Docker cgroups — it does not use NVIDIA_API_KEY. - run(`sudo bash "${SCRIPTS}/setup-spark.sh"`); + await ensureApiKey(); + run(`sudo -E bash "${SCRIPTS}/setup-spark.sh"`, { + env: { NVIDIA_API_KEY: process.env.NVIDIA_API_KEY }, + }); } // eslint-disable-next-line complexity diff --git a/install.sh b/install.sh index a902d2c91..7860bcbb8 100755 --- a/install.sh +++ b/install.sh @@ -679,7 +679,14 @@ install_nemoclaw() { || warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken" spin "Installing NemoClaw dependencies" npm install --ignore-scripts spin "Building NemoClaw plugin" bash -c 'cd nemoclaw && npm install --ignore-scripts && npm run build' - spin "Linking NemoClaw CLI" npm link + # Use sudo for npm link only when the global prefix is not writable + local npm_global_prefix + npm_global_prefix="$(npm config get prefix 2>/dev/null)" || true + local sudo_cmd="" + if [ -n "$npm_global_prefix" ] && [ ! -w "$npm_global_prefix" ] && [ "$(id -u)" -ne 0 ]; then + sudo_cmd="sudo" + fi + spin "Linking NemoClaw CLI" $sudo_cmd npm link else info "Installing NemoClaw from GitHub…" # Resolve the latest release tag so we never install raw main. @@ -697,7 +704,14 @@ install_nemoclaw() { || warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken" spin "Installing NemoClaw dependencies" bash -c "cd \"$nemoclaw_src\" && npm install --ignore-scripts" spin "Building NemoClaw plugin" bash -c "cd \"$nemoclaw_src\"/nemoclaw && npm install --ignore-scripts && npm run build" - spin "Linking NemoClaw CLI" bash -c "cd \"$nemoclaw_src\" && npm link" + # Use sudo for npm link only when the global prefix is not writable + local npm_global_prefix + npm_global_prefix="$(npm config get prefix 2>/dev/null)" || true + local sudo_cmd="" + if [ -n "$npm_global_prefix" ] && [ ! -w "$npm_global_prefix" ] && [ "$(id -u)" -ne 0 ]; then + sudo_cmd="sudo" + fi + spin "Linking NemoClaw CLI" bash -c "cd \"$nemoclaw_src\" && $sudo_cmd npm link" fi refresh_path diff --git a/nemoclaw-blueprint/policies/presets/local-inference.yaml b/nemoclaw-blueprint/policies/presets/local-inference.yaml new file mode 100644 index 000000000..c692cdce2 --- /dev/null +++ b/nemoclaw-blueprint/policies/presets/local-inference.yaml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +preset: + name: local-inference + description: "Local inference access (Ollama, vLLM) via host gateway" + +network_policies: + local_inference: + name: local_inference + endpoints: + - host: host.openshell.internal + port: 11434 + protocol: rest + enforcement: enforce + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + - host: host.openshell.internal + port: 8000 + protocol: rest + enforcement: enforce + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + binaries: + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/claude } diff --git a/scripts/telegram-bridge.js b/scripts/telegram-bridge.js index 27d5d7ba4..c7ee8c681 100755 --- a/scripts/telegram-bridge.js +++ b/scripts/telegram-bridge.js @@ -105,13 +105,13 @@ function runAgentInSandbox(message, sessionId) { const confPath = `${confDir}/config`; require("fs").writeFileSync(confPath, sshConfig, { mode: 0o600 }); - // Pass message and API key via stdin to avoid shell interpolation. - // The remote command reads them from environment/stdin rather than - // embedding user content in a shell string. + // Pass API key via SendEnv + ssh config to avoid exposing it in + // process arguments (visible in ps aux / /proc/*/cmdline). const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9-]/g, ""); - const cmd = `export NVIDIA_API_KEY=${shellQuote(API_KEY)} && nemoclaw-start openclaw agent --agent main --local -m ${shellQuote(message)} --session-id ${shellQuote("tg-" + safeSessionId)}`; + const cmd = `nemoclaw-start openclaw agent --agent main --local -m ${shellQuote(message)} --session-id ${shellQuote("tg-" + safeSessionId)}`; - const proc = spawn("ssh", ["-T", "-F", confPath, `openshell-${SANDBOX}`, cmd], { + const proc = spawn("ssh", ["-T", "-F", confPath, "-o", "SendEnv=NVIDIA_API_KEY", `openshell-${SANDBOX}`, cmd], { + env: { ...process.env, NVIDIA_API_KEY: API_KEY }, timeout: 120000, stdio: ["ignore", "pipe", "pipe"], }); diff --git a/scripts/walkthrough.sh b/scripts/walkthrough.sh index fe176220d..90b7b7d5b 100755 --- a/scripts/walkthrough.sh +++ b/scripts/walkthrough.sh @@ -84,10 +84,9 @@ tmux kill-session -t "$SESSION" 2>/dev/null || true # Create session with TUI on the left tmux new-session -d -s "$SESSION" -x 200 -y 50 "openshell term" -# Split right pane for the agent -# NVIDIA_API_KEY is not needed inside the sandbox — inference is proxied -# through the OpenShell gateway which injects credentials server-side. -tmux split-window -h -t "$SESSION" \ +# Split right pane for the agent — pass API key via env to avoid leaking +# the secret in process arguments (visible in ps aux / /proc/*/cmdline). +tmux split-window -h -t "$SESSION" -e "NVIDIA_API_KEY=$NVIDIA_API_KEY" \ "openshell sandbox connect nemoclaw -- bash -c 'nemoclaw-start openclaw agent --agent main --local --session-id live'" # Even split diff --git a/spark-install.md b/spark-install.md index 34a6702bd..946358e95 100644 --- a/spark-install.md +++ b/spark-install.md @@ -208,7 +208,18 @@ The OpenClaw gateway includes a built-in web UI. Access it at: http://127.0.0.1:18789/#token= ``` -Find your gateway token in `~/.openclaw/openclaw.json` under `gateway.auth.token` inside the sandbox. +To retrieve your gateway token, run this from inside the sandbox: + +```console +$ jq -r '.gateway.auth.token' /sandbox/.openclaw/openclaw.json +``` + +Or from the host, download the config first: + +```console +$ openshell sandbox download /sandbox/.openclaw/openclaw.json ./openclaw.json +$ jq -r '.gateway.auth.token' ./openclaw.json +``` > **Important**: Use `127.0.0.1` (not `localhost`) — the gateway's origin check requires an exact match. External dashboards like Mission Control cannot currently connect due to the gateway resetting `controlUi.allowedOrigins` on every config reload (see [openclaw#49950](https://github.com/openclaw/openclaw/issues/49950)). diff --git a/test/policies.test.js b/test/policies.test.js index cd2840655..362ab646d 100644 --- a/test/policies.test.js +++ b/test/policies.test.js @@ -5,11 +5,13 @@ import assert from "node:assert/strict"; import { describe, it, expect } from "vitest"; import policies from "../bin/lib/policies"; +const expectedPresets = ["discord", "docker", "huggingface", "jira", "local-inference", "npm", "outlook", "pypi", "slack", "telegram"]; + describe("policies", () => { describe("listPresets", () => { - it("returns all 9 presets", () => { + it(`returns all ${expectedPresets.length} presets`, () => { const presets = policies.listPresets(); - expect(presets.length).toBe(9); + expect(presets.length).toBe(expectedPresets.length); }); it("each preset has name and description", () => { @@ -20,22 +22,8 @@ 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", - ]; - expect(names).toEqual(expected); + const names = policies.listPresets().map((p) => p.name).sort(); + expect(names).toEqual(expectedPresets); }); }); @@ -296,6 +284,33 @@ describe("policies", () => { }); }); + describe("local-inference preset", () => { + it("loads and contains host.openshell.internal", () => { + const content = policies.loadPreset("local-inference"); + expect(content).toBeTruthy(); + const hosts = policies.getPresetEndpoints(content); + expect(hosts.includes("host.openshell.internal")).toBeTruthy(); + }); + + it("allows Ollama port 11434 and vLLM port 8000", () => { + const content = policies.loadPreset("local-inference"); + expect(content.includes("port: 11434")).toBe(true); + expect(content.includes("port: 8000")).toBe(true); + }); + + it("has a binaries section", () => { + const content = policies.loadPreset("local-inference"); + expect(content.includes("binaries:")).toBe(true); + }); + + it("extracts valid network_policies entries", () => { + const content = policies.loadPreset("local-inference"); + const entries = policies.extractPresetEntries(content); + expect(entries).toBeTruthy(); + expect(entries.includes("local_inference")).toBe(true); + }); + }); + describe("preset YAML schema", () => { it("no preset has rules at NetworkPolicyRuleDef level", () => { // rules must be inside endpoints, not as sibling of endpoints/binaries