Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
10 changes: 9 additions & 1 deletion bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2394,11 +2394,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 @@ -358,8 +358,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 @@ -569,7 +569,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 @@ -587,7 +594,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 }
5 changes: 5 additions & 0 deletions research/results.tsv
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.

Should this have been .gitignored?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hello, it was .gitignored but still tracked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — yes, research/ was already in .gitignore but this file was tracked before the rule was added. Removed it from the index in the latest push.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
experiment track description tests_total tests_pass tests_fail notes
0 baseline initial state — 194 tests, 188 pass, 6 fail 194 188 6 6 failures in installer preflight + uninstall CLI; 30 open issues; coverage: 93% lines, 98% functions, 87% branches
1 track1 fix 3 remaining test failures: uninstall symlink rm, installer sudo heuristic 194 194 0 uninstall.sh: check dir writability not symlink target; scripts/install.sh: check npm prefix writability instead of assuming nodesource needs sudo
2 track2 fix #579/#664: add redactSecrets to runner.js, fix API key exposure in setupSpark/walkthrough/telegram-bridge 203 203 0 added redactSecrets() with 4 patterns (env assignments, nvapi-, ghp_, Bearer); error messages now redacted; setupSpark passes key via env not cmdline; walkthrough.sh uses tmux -e; telegram-bridge uses SSH SendEnv; 9 new tests
3 track3 fix #693: add local-inference policy preset for Ollama/vLLM host gateway access 207 207 0 new local-inference.yaml preset allows host.openshell.internal on ports 11434 (Ollama) and 8000 (vLLM) with binaries restriction; onboard auto-suggests preset when local provider selected; 4 new tests
10 changes: 5 additions & 5 deletions scripts/telegram-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,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 @@ -171,6 +159,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