Skip to content
Open
10 changes: 9 additions & 1 deletion bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
33 changes: 30 additions & 3 deletions bin/lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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=<REDACTED>
* - NVIDIA API key prefix: nvapi-Abc123... → <REDACTED>
* - GitHub PAT prefix: ghp_Abc123... → <REDACTED>
* - Bearer tokens: Bearer eyJhb... → Bearer <REDACTED>
*/
const SECRET_PATTERNS = [
{ re: /(NVIDIA_API_KEY|API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|_KEY)=\S+/gi, replacement: "$1=<REDACTED>" },
{ re: /nvapi-[A-Za-z0-9_-]{10,}/g, replacement: "<REDACTED>" },
{ re: /ghp_[A-Za-z0-9]{30,}/g, replacement: "<REDACTED>" },
{ re: /(Bearer )\S+/gi, replacement: "$1<REDACTED>" },
];

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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 };
6 changes: 4 additions & 2 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions nemoclaw-blueprint/policies/presets/local-inference.yaml
Original file line number Diff line number Diff line change
@@ -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 }
10 changes: 5 additions & 5 deletions scripts/telegram-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
});
Expand Down
7 changes: 3 additions & 4 deletions scripts/walkthrough.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion spark-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,18 @@ The OpenClaw gateway includes a built-in web UI. Access it at:
http://127.0.0.1:18789/#token=<your-gateway-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-name> /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)).

Expand Down
51 changes: 33 additions & 18 deletions test/policies.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -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
Expand Down
Loading