Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
9 changes: 9 additions & 0 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const registry = require("./registry");
const nim = require("./nim");
const onboardSession = require("./onboard-session");
const policies = require("./policies");
const { ensureUsageNoticeConsent } = require("./usage-notice");
const { checkPortAvailable, ensureSwap, getMemoryInfo } = require("./preflight");

// Typed modules (compiled from src/lib/*.ts → dist/lib/*.js)
Expand Down Expand Up @@ -3450,6 +3451,14 @@ async function onboard(opts = {}) {
NON_INTERACTIVE = opts.nonInteractive || process.env.NEMOCLAW_NON_INTERACTIVE === "1";
delete process.env.OPENSHELL_GATEWAY;
const resume = opts.resume === true;
const noticeAccepted = await ensureUsageNoticeConsent({
nonInteractive: isNonInteractive(),
acceptedByFlag: opts.acceptThirdPartySoftware === true,
writeLine: console.error,
});
if (!noticeAccepted) {
process.exit(1);
}
const lockResult = onboardSession.acquireOnboardLock(
`nemoclaw onboard${resume ? " --resume" : ""}${isNonInteractive() ? " --non-interactive" : ""}`,
);
Expand Down
158 changes: 158 additions & 0 deletions bin/lib/usage-notice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const fs = require("fs");
const os = require("os");
const path = require("path");

const NOTICE_ACCEPT_FLAG = "--yes-i-accept-third-party-software";
const NOTICE_ACCEPT_ENV = "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE";
const NOTICE_CONFIG_FILE = path.join(__dirname, "usage-notice.json");
const OSC8_OPEN = "\u001B]8;;";
const OSC8_CLOSE = "\u001B]8;;\u001B\\";
const OSC8_TERM = "\u001B\\";

function getUsageNoticeStateFile() {
return path.join(process.env.HOME || os.homedir(), ".nemoclaw", "usage-notice.json");
}

function loadUsageNoticeConfig() {
return JSON.parse(fs.readFileSync(NOTICE_CONFIG_FILE, "utf8"));
}

function hasAcceptedUsageNotice(version) {
try {
const saved = JSON.parse(fs.readFileSync(getUsageNoticeStateFile(), "utf8"));
return saved?.acceptedVersion === version;
} catch {
return false;
}
}

function saveUsageNoticeAcceptance(version) {
const stateFile = getUsageNoticeStateFile();
const dir = path.dirname(stateFile);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
fs.chmodSync(dir, 0o700);
fs.writeFileSync(
stateFile,
JSON.stringify({ acceptedVersion: version, acceptedAt: new Date().toISOString() }, null, 2),
{ mode: 0o600 },
);
fs.chmodSync(stateFile, 0o600);
}

function supportsTerminalHyperlinks() {
const tty = process.stderr?.isTTY || process.stdout?.isTTY;
if (!tty) return false;
if (process.env.NO_COLOR) return false;
if (process.env.TERM === "dumb") return false;
return true;
}

function formatTerminalHyperlink(label, url) {
return `${OSC8_OPEN}${url}${OSC8_TERM}${label}${OSC8_CLOSE}`;
}

function printUsageNotice(config = loadUsageNoticeConfig(), writeLine = console.error) {
writeLine("");
writeLine(` ${config.title}`);
writeLine(" ──────────────────────────────────────────────────");
for (const line of config.body || []) {
writeLine(` ${line}`);
}
for (const link of config.links || []) {
writeLine("");
const label =
supportsTerminalHyperlinks() && link?.url && link?.label
? formatTerminalHyperlink(link.label, link.url)
: link?.label || "";
if (label) {
writeLine(` ${label}`);
}
if (link?.url) {
writeLine(` ${link.url}`);
}
}
writeLine("");
}

async function ensureUsageNoticeConsent({
nonInteractive = false,
acceptedByFlag = false,
promptFn = null,
writeLine = console.error,
} = {}) {
const config = loadUsageNoticeConfig();
if (hasAcceptedUsageNotice(config.version)) {
return true;
}

printUsageNotice(config, writeLine);

if (nonInteractive) {
if (!acceptedByFlag) {
writeLine(
` Non-interactive onboarding requires ${NOTICE_ACCEPT_FLAG} or ${NOTICE_ACCEPT_ENV}=1.`,
);
return false;
}
writeLine(
` [non-interactive] Third-party software notice accepted via ${NOTICE_ACCEPT_FLAG}.`,
);
saveUsageNoticeAcceptance(config.version);
return true;
}

if (!process.stdin.isTTY) {
writeLine(
` Interactive onboarding requires a TTY. Re-run in a terminal or use --non-interactive with ${NOTICE_ACCEPT_FLAG}.`,
);
return false;
}

const ask = promptFn || require("./credentials").prompt;
const answer = String(await ask(` ${config.interactivePrompt}`))
.trim()
.toLowerCase();
if (answer !== "yes") {
writeLine(" Installation cancelled.");
return false;
}

saveUsageNoticeAcceptance(config.version);
return true;
}

async function cli(args = process.argv.slice(2)) {
const acceptedByFlag =
args.includes(NOTICE_ACCEPT_FLAG) || String(process.env[NOTICE_ACCEPT_ENV] || "") === "1";
const nonInteractive = args.includes("--non-interactive");
const ok = await ensureUsageNoticeConsent({
nonInteractive,
acceptedByFlag,
writeLine: console.error,
});
process.exit(ok ? 0 : 1);
}

if (require.main === module) {
cli().catch((error) => {
console.error(error?.message || String(error));
process.exit(1);
});
}

module.exports = {
NOTICE_ACCEPT_ENV,
NOTICE_ACCEPT_FLAG,
NOTICE_CONFIG_FILE,
ensureUsageNoticeConsent,
getUsageNoticeStateFile,
hasAcceptedUsageNotice,
loadUsageNoticeConfig,
formatTerminalHyperlink,
printUsageNotice,
saveUsageNoticeAcceptance,
supportsTerminalHyperlinks,
};
19 changes: 19 additions & 0 deletions bin/lib/usage-notice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "2026-04-01b",
"title": "Third-Party Software Notice",
"referenceUrl": "https://docs.openclaw.ai/gateway/security",
"body": [
"NemoClaw deploys OpenClaw in an OpenShell sandbox.",
"It may also install or run other third-party software.",
"By continuing, you accept responsibility for:",
"- reviewing third-party terms, licenses, and security guidance",
"- complying with those requirements"
],
"links": [
{
"label": "OpenClaw security guidance",
"url": "https://docs.openclaw.ai/gateway/security"
}
],
"interactivePrompt": "Type 'yes' to accept this third-party software notice and continue [no]: "
}
12 changes: 9 additions & 3 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const { parseGatewayInference } = require("./lib/inference-config");
const { getVersion } = require("./lib/version");
const onboardSession = require("./lib/onboard-session");
const { parseLiveSandboxNames } = require("./lib/runtime-recovery");
const { NOTICE_ACCEPT_ENV, NOTICE_ACCEPT_FLAG } = require("./lib/usage-notice");

// ── Global commands ──────────────────────────────────────────────

Expand Down Expand Up @@ -622,16 +623,20 @@ function exitWithSpawnResult(result) {

async function onboard(args) {
const { onboard: runOnboard } = require("./lib/onboard");
const allowedArgs = new Set(["--non-interactive", "--resume"]);
const allowedArgs = new Set(["--non-interactive", "--resume", NOTICE_ACCEPT_FLAG]);
const unknownArgs = args.filter((arg) => !allowedArgs.has(arg));
if (unknownArgs.length > 0) {
console.error(` Unknown onboard option(s): ${unknownArgs.join(", ")}`);
console.error(" Usage: nemoclaw onboard [--non-interactive] [--resume]");
console.error(
` Usage: nemoclaw onboard [--non-interactive] [--resume] [${NOTICE_ACCEPT_FLAG}]`,
);
process.exit(1);
}
const nonInteractive = args.includes("--non-interactive");
const resume = args.includes("--resume");
await runOnboard({ nonInteractive, resume });
const acceptThirdPartySoftware =
args.includes(NOTICE_ACCEPT_FLAG) || String(process.env[NOTICE_ACCEPT_ENV] || "") === "1";
await runOnboard({ nonInteractive, resume, acceptThirdPartySoftware });
}

async function setup(args = []) {
Expand Down Expand Up @@ -1154,6 +1159,7 @@ function help() {

${G}Getting Started:${R}
${B}nemoclaw onboard${R} Configure inference endpoint and credentials
${D}(non-interactive: ${NOTICE_ACCEPT_FLAG})${R}
nemoclaw setup-spark Set up on DGX Spark ${D}(fixes cgroup v2 + Docker)${R}

${G}Sandbox Management:${R}
Expand Down
33 changes: 32 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,12 @@ usage() {
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -s -- [options]\n\n"
printf " ${C_DIM}Options:${C_RESET}\n"
printf " --non-interactive Skip prompts (uses env vars / defaults)\n"
printf " --yes-i-accept-third-party-software Accept the third-party software notice in non-interactive mode\n"
printf " --version, -v Print installer version and exit\n"
printf " --help, -h Show this help message and exit\n\n"
printf " ${C_DIM}Environment:${C_RESET}\n"
printf " NVIDIA_API_KEY API key (skips credential prompt)\n"
printf " NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 Same as --yes-i-accept-third-party-software\n"
printf " NEMOCLAW_NON_INTERACTIVE=1 Same as --non-interactive\n"
printf " NEMOCLAW_SANDBOX_NAME Sandbox name to create/use\n"
printf " NEMOCLAW_RECREATE_SANDBOX=1 Recreate an existing sandbox\n"
Expand All @@ -237,6 +239,27 @@ usage() {
printf "\n"
}

show_usage_notice() {
local -a notice_cmd=(node "${SCRIPT_DIR}/bin/lib/usage-notice.js")
if [ "${NON_INTERACTIVE:-}" = "1" ]; then
notice_cmd+=(--non-interactive)
if [ "${ACCEPT_THIRD_PARTY_SOFTWARE:-}" = "1" ]; then
notice_cmd+=(--yes-i-accept-third-party-software)
fi
"${notice_cmd[@]}"
elif [ -t 0 ]; then
"${notice_cmd[@]}"
elif exec 3</dev/tty; then
info "Installer stdin is piped; attaching the usage notice to /dev/tty…"
local status=0
"${notice_cmd[@]}" <&3 || status=$?
exec 3<&-
return "$status"
else
error "Interactive third-party software acceptance requires a TTY. Re-run in a terminal or set NEMOCLAW_NON_INTERACTIVE=1 with --yes-i-accept-third-party-software."
fi
}

# spin "label" cmd [args...]
# Runs a command in the background, showing a braille spinner until it exits.
# Stdout/stderr are captured; dumped only on failure.
Expand Down Expand Up @@ -806,6 +829,7 @@ verify_nemoclaw() {
# 5. Onboard
# ---------------------------------------------------------------------------
run_onboard() {
show_usage_notice
info "Running nemoclaw onboard…"
local -a onboard_cmd=(onboard)
if command_exists node && [[ -f "${HOME}/.nemoclaw/onboard-session.json" ]]; then
Expand All @@ -827,6 +851,9 @@ run_onboard() {
fi
if [ "${NON_INTERACTIVE:-}" = "1" ]; then
onboard_cmd+=(--non-interactive)
if [ "${ACCEPT_THIRD_PARTY_SOFTWARE:-}" = "1" ]; then
onboard_cmd+=(--yes-i-accept-third-party-software)
fi
nemoclaw "${onboard_cmd[@]}"
elif [ -t 0 ]; then
nemoclaw "${onboard_cmd[@]}"
Expand All @@ -837,7 +864,7 @@ run_onboard() {
exec 3<&-
return "$status"
else
error "Interactive onboarding requires a TTY. Re-run in a terminal or set NEMOCLAW_NON_INTERACTIVE=1."
error "Interactive onboarding requires a TTY. Re-run in a terminal or set NEMOCLAW_NON_INTERACTIVE=1 with --yes-i-accept-third-party-software."
fi
}

Expand Down Expand Up @@ -880,9 +907,11 @@ post_install_message() {
main() {
# Parse flags
NON_INTERACTIVE=""
ACCEPT_THIRD_PARTY_SOFTWARE=""
for arg in "$@"; do
case "$arg" in
--non-interactive) NON_INTERACTIVE=1 ;;
--yes-i-accept-third-party-software) ACCEPT_THIRD_PARTY_SOFTWARE=1 ;;
--version | -v)
printf "nemoclaw-installer v%s\n" "$NEMOCLAW_VERSION"
exit 0
Expand All @@ -899,7 +928,9 @@ main() {
done
# Also honor env var
NON_INTERACTIVE="${NON_INTERACTIVE:-${NEMOCLAW_NON_INTERACTIVE:-}}"
ACCEPT_THIRD_PARTY_SOFTWARE="${ACCEPT_THIRD_PARTY_SOFTWARE:-${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}}"
export NEMOCLAW_NON_INTERACTIVE="${NON_INTERACTIVE}"
export NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE="${ACCEPT_THIRD_PARTY_SOFTWARE}"

_INSTALL_START=$SECONDS
print_banner
Expand Down
8 changes: 7 additions & 1 deletion test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ describe("CLI dispatch", () => {
expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy();
});

it("accepts the third-party software flag in onboard CLI parsing", () => {
const r = run("onboard --yes-i-accept-third-party-software --non-interactiv");
expect(r.code).toBe(1);
expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy();
});

it("setup forwards unknown options into onboard parsing", () => {
const r = run("setup --non-interactiv");
expect(r.code).toBe(1);
Expand All @@ -134,7 +140,7 @@ describe("CLI dispatch", () => {
});

it("setup forwards --resume into onboard parsing", () => {
const r = run("setup --resume");
const r = run("setup --resume --non-interactive --yes-i-accept-third-party-software");
expect(r.code).toBe(1);
expect(r.out.includes("deprecated")).toBeTruthy();
expect(r.out.includes("No resumable onboarding session was found")).toBeTruthy();
Expand Down
Loading
Loading