Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
15 changes: 15 additions & 0 deletions bin/lib/usage-notice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Thin re-export shim — the implementation lives in src/lib/usage-notice.ts,
// compiled to dist/lib/usage-notice.js.
const usageNotice = require("../../dist/lib/usage-notice");

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

module.exports = usageNotice;
32 changes: 32 additions & 0 deletions bin/lib/usage-notice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"version": "2026-04-01b",
"title": "Third-Party Software Notice - NemoClaw Installer",
"referenceUrl": "https://docs.openclaw.ai/gateway/security",
"body": [
"NemoClaw is licensed under Apache 2.0 and automatically",
"retrieves, accesses or interacts with third-party software",
"and materials, including by deploying OpenClaw in an",
"OpenShell sandbox. Those retrieved materials are not",
"distributed with this software and are governed solely",
"by separate terms, conditions and licenses.",
"",
"You are solely responsible for finding, reviewing and",
"complying with all applicable terms, conditions, and",
"licenses, and for verifying the security, integrity and",
"suitability of any retrieved materials for your specific",
"use case.",
"",
"This software is provided \"AS IS\", without warranty of",
"any kind. The author makes no representations or",
"warranties regarding any third-party software, and",
"assumes no liability for any losses, damages, liabilities",
"or legal consequences from your use or inability to use",
"this software or any retrieved materials. Use this",
"software and the retrieved materials at your own risk.",
"",
"OpenClaw security guidance",
"https://docs.openclaw.ai/gateway/security"
],
"links": [],
"interactivePrompt": "Type 'yes' to accept the NemoClaw license and and 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
175 changes: 175 additions & 0 deletions src/lib/usage-notice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import noticeConfig from "../../bin/lib/usage-notice.json";

export const NOTICE_ACCEPT_FLAG = "--yes-i-accept-third-party-software";
export const NOTICE_ACCEPT_ENV = "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE";
export const NOTICE_CONFIG_FILE = path.join(__dirname, "..", "..", "bin", "lib", "usage-notice.json");

const OSC8_OPEN = "\u001B]8;;";
const OSC8_CLOSE = "\u001B]8;;\u001B\\";
const OSC8_TERM = "\u001B\\";

type NoticeLink = {
label?: string;
url?: string;
};

type NoticeConfig = {
version: string;
title: string;
referenceUrl?: string;
body?: string[];
links?: NoticeLink[];
interactivePrompt: string;
};

type PromptFn = (question: string) => Promise<string>;
type WriteLineFn = (line: string) => void;

type EnsureUsageNoticeConsentOptions = {
nonInteractive?: boolean;
acceptedByFlag?: boolean;
promptFn?: PromptFn | null;
writeLine?: WriteLineFn;
};

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

export function loadUsageNoticeConfig(): NoticeConfig {
return noticeConfig as NoticeConfig;
}

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

export function saveUsageNoticeAcceptance(version: string): void {
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);
}

export function supportsTerminalHyperlinks(): boolean {
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;
}

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

export function printUsageNotice(
config: NoticeConfig = loadUsageNoticeConfig(),
writeLine: WriteLineFn = console.error,
): void {
writeLine("");
writeLine(` ${config.title}`);
writeLine(" ──────────────────────────────────────────────────");
for (const line of config.body || []) {
const renderedLine =
/^https?:\/\//.test(line) && supportsTerminalHyperlinks()
? formatTerminalHyperlink(line, line)
: line;
writeLine(` ${renderedLine}`);
}
for (const link of config.links || []) {
writeLine("");
const label =
supportsTerminalHyperlinks() && link?.url && link?.label
? formatTerminalHyperlink(link.url, link.url)
: link?.label || "";
if (label) {
writeLine(` ${label}`);
}
if (link?.url) {
writeLine(` ${link.url}`);
}
}
writeLine("");
}

export async function ensureUsageNoticeConsent({
nonInteractive = false,
acceptedByFlag = false,
promptFn = null,
writeLine = console.error,
}: EnsureUsageNoticeConsentOptions = {}): Promise<boolean> {
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;
}

// credentials is still CJS
// eslint-disable-next-line @typescript-eslint/no-require-imports
const ask = promptFn || require("../../bin/lib/credentials").prompt;
const answer = String(await ask(` ${config.interactivePrompt}`))
.trim()
.toLowerCase();
if (answer !== "yes") {
writeLine(" Installation cancelled");
return false;
}

saveUsageNoticeAcceptance(config.version);
return true;
}

export async function cli(args = process.argv.slice(2)): Promise<void> {
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);
}
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