Skip to content
Merged
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ The script installs Node.js if it is not already present, then runs the guided o
curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

The piped installer prompts through your terminal. In headless scripts or CI,
pass explicit acceptance to the `bash` side of the pipe:

```bash
curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 bash
```

If you use nvm or fnm to manage Node.js, the installer may not update your current shell's PATH.
If `nemoclaw` is not found after install, run `source ~/.bashrc` (or `source ~/.zshrc` for zsh) or open a new terminal.

Expand Down
7 changes: 7 additions & 0 deletions docs/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ NemoClaw creates a fresh OpenClaw instance inside the sandbox during the onboard
curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

The piped installer prompts through your terminal. In headless scripts or CI,
pass explicit acceptance to the `bash` side of the pipe:

```console
$ curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 bash
```

If you use nvm or fnm to manage Node.js, the installer might not update your current shell's PATH.
If `nemoclaw` is not found after install, run `source ~/.bashrc` (or `source ~/.zshrc` for zsh) or open a new terminal.

Expand Down
7 changes: 6 additions & 1 deletion docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ or:
$ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive
```

For scripted installer runs, set `NEMOCLAW_NON_INTERACTIVE=1` and `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1` before invoking `curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash`.
For scripted installer runs, pass explicit acceptance to the `bash` side of the installer pipe:

```console
$ curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 bash
```

If the installer cannot prompt for the notice in a terminal and no explicit acceptance is set, it exits before installing Node.js or the NemoClaw CLI.

To enable Brave Search in non-interactive mode, set:
Expand Down
157 changes: 139 additions & 18 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,140 @@ show_usage_notice() {
fi
}

usage_notice_config_path() {
local repo_root source_root notice_json
repo_root="$(resolve_repo_root)"
source_root="${NEMOCLAW_SOURCE_ROOT:-$repo_root}"
notice_json="${source_root}/bin/lib/usage-notice.json"
if [[ ! -f "$notice_json" ]]; then
notice_json="${repo_root}/bin/lib/usage-notice.json"
fi
printf "%s" "$notice_json"
}

json_string_field() {
local file="$1" field="$2"
sed -nE "s/^[[:space:]]*\"${field}\"[[:space:]]*:[[:space:]]*\"(.*)\"[,]?[[:space:]]*$/\\1/p" "$file" \
| head -n 1 \
| sed 's/\\"/"/g; s/\\\\/\\/g'
}

usage_notice_state_file() {
printf "%s/.nemoclaw/usage-notice.json" "${HOME}"
}

usage_notice_accepted_shell() {
local version="$1" state_file saved_version
state_file="$(usage_notice_state_file)"
[[ -n "$version" && -f "$state_file" ]] || return 1
saved_version="$(sed -nE 's/.*"acceptedVersion"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$state_file" | head -n 1)"
[[ "$saved_version" == "$version" ]]
}

save_usage_notice_acceptance_shell() {
local version="$1" state_file state_dir accepted_at
state_file="$(usage_notice_state_file)"
state_dir="$(dirname "$state_file")"
accepted_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date)"
mkdir -p "$state_dir"
chmod 700 "$state_dir" 2>/dev/null || true
printf '{\n "acceptedVersion": "%s",\n "acceptedAt": "%s"\n}\n' "$version" "$accepted_at" >"$state_file"
chmod 600 "$state_file" 2>/dev/null || true
}

print_usage_notice_body_shell() {
local file="$1"
awk '
/"body"[[:space:]]*:/ { in_body = 1; next }
in_body && /^[[:space:]]*]/ { exit }
in_body {
line = $0
sub(/^[[:space:]]*"/, "", line)
sub(/",[[:space:]]*$/, "", line)
sub(/"[[:space:]]*$/, "", line)
gsub(/\\"/, "\"", line)
gsub(/\\\\/, "\\", line)
printf " %s\n", line
}
' "$file"
}

show_usage_notice_shell() {
local notice_json version title prompt notice_body answer answer_lc
notice_json="$(usage_notice_config_path)"
if [[ ! -f "$notice_json" ]]; then
error "Third-party software notice configuration not found."
fi

version="$(json_string_field "$notice_json" "version")"
title="$(json_string_field "$notice_json" "title")"
prompt="$(json_string_field "$notice_json" "interactivePrompt")"
if [[ -z "$version" ]]; then
error "Third-party software notice version not found."
fi
notice_body="$(print_usage_notice_body_shell "$notice_json")"
if [[ -z "$(printf "%s" "$notice_body" | tr -d '[:space:]')" ]]; then
error "Third-party software notice body not found."
fi

if usage_notice_accepted_shell "$version"; then
return 0
fi

printf "\n"
printf " %s\n" "${title:-Third-Party Software Notice - NemoClaw Installer}"
printf " ──────────────────────────────────────────────────\n"
printf "%s\n" "$notice_body"
printf "\n"
printf " %s" "${prompt:-Type 'yes' to accept the NemoClaw license and third-party software notice and continue [no]: }"
if ! IFS= read -r answer; then
printf "\n Installation cancelled\n" >&2
return 1
fi
answer_lc="$(printf "%s" "$answer" | tr '[:upper:]' '[:lower:]')"
if [[ "$answer_lc" != "yes" ]]; then
printf " Installation cancelled\n" >&2
return 1
fi

save_usage_notice_acceptance_shell "$version"
return 0
}

preflight_usage_notice_prompt() {
if [ "${ACCEPT_THIRD_PARTY_SOFTWARE:-}" = "1" ]; then
return 0
fi
Comment on lines +683 to +685
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Persist explicit acceptance as well.

Line 683 short-circuits before save_usage_notice_acceptance_shell(), so the documented headless path (NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 / --yes-i-accept-third-party-software) never writes ~/.nemoclaw/usage-notice.json. Users who accept via env/flag will be prompted again on the next run even though this flow is now versioned and stateful.

Suggested fix
 preflight_usage_notice_prompt() {
   if [ "${ACCEPT_THIRD_PARTY_SOFTWARE:-}" = "1" ]; then
+    local notice_json version
+    notice_json="$(usage_notice_config_path)"
+    if [[ -f "$notice_json" ]]; then
+      version="$(json_string_field "$notice_json" "version")"
+      if [[ -n "$version" ]]; then
+        save_usage_notice_acceptance_shell "$version"
+      fi
+    fi
     return 0
   fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install.sh` around lines 683 - 685, The early return when
ACCEPT_THIRD_PARTY_SOFTWARE is set prevents persisting acceptance, so modify the
branch that checks the ACCEPT_THIRD_PARTY_SOFTWARE /
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE flag to call
save_usage_notice_acceptance_shell() (or whatever persistence helper is used)
before returning; ensure the code path that exits early still invokes the same
persistence function used for interactive acceptance so
~/.nemoclaw/usage-notice.json is written and state is versioned.


local notice_json version
notice_json="$(usage_notice_config_path)"
if [[ -f "$notice_json" ]]; then
version="$(json_string_field "$notice_json" "version")"
if [[ -n "$version" ]] && usage_notice_accepted_shell "$version"; then
return 0
fi
fi

if [ "${NON_INTERACTIVE:-}" = "1" ]; then
error "Non-interactive installation requires explicit third-party software acceptance. Re-run with --yes-i-accept-third-party-software or set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1."
fi

if [ -t 0 ]; then
show_usage_notice_shell
return "$?"
fi

if { exec 3</dev/tty; } 2>/dev/null; then
info "Installer stdin is piped; prompting for the third-party software notice on /dev/tty before install."
local status=0
show_usage_notice_shell <&3 || status=$?
exec 3<&-
return "$status"
fi

error "Interactive third-party software acceptance requires a TTY. Re-run in a terminal or pass --yes-i-accept-third-party-software (or set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1)."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +696 to +713
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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This closes off the existing FIFO-driven smoke install path.

scripts/smoke-macos-install.sh:205-212 still drives install.sh with stdin from a named pipe and no controlling TTY. With this preflight, that path now exits before phase 1. The obvious workaround (NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1) still changes behavior, because main() later coerces explicit acceptance into NON_INTERACTIVE=1, so the smoke harness can no longer feed the remaining prompts from its answers pipe. Please update that harness in the same PR, or preserve a supported scripted-input path here.

Also applies to: 1701-1705

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install.sh` around lines 696 - 713, The new preflight blocks reject
the existing FIFO-driven smoke install path by requiring a TTY; update the logic
so scripted/stdin-piped installs are still supported: when exec 3</dev/tty
fails, check whether stdin is a pipe (i.e. [ -t 0 ] is false) and if so call
show_usage_notice_shell reading from stdin (or from the piped FD) instead of
erroring; additionally avoid conflating NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE
with NON_INTERACTIVE in main()—keep acceptance from the env/flag separate so the
smoke harness can set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 without the
installer switching to NON_INTERACTIVE and stopping further stdin-driven prompts
(update the handling in main() and retain show_usage_notice_shell usage for
piped stdin).

}

# 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 @@ -1564,24 +1698,11 @@ main() {
export NEMOCLAW_NON_INTERACTIVE="${NON_INTERACTIVE}"
export NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE="${ACCEPT_THIRD_PARTY_SOFTWARE}"

# Fail-fast license-acceptance check (#2671). If we already know phase 3
# (show_usage_notice + run_onboard) will hit the "requires a TTY" branch,
# surface that error NOW — before phases 1/2 install Node.js and put the
# nemoclaw CLI on PATH. Otherwise the user is left in a partial install
# that they have to manually `rm -rf` before retry, while their license
# has not actually been accepted.
#
# Skipped (and the install proceeds) only when either:
# - NON_INTERACTIVE=1 (also implied by ACCEPT_THIRD_PARTY_SOFTWARE=1 above)
# - stdin is a TTY — license helper prompts the user directly before install
#
# Do not treat an openable /dev/tty as sufficient here. In curl|bash mode,
# stdin is a pipe even though /dev/tty may still be available; falling back to
# /dev/tty later would run phases 1/2 before the license prompt and could leave
# a partial install behind if the user declines or no terminal is attached.
if [ "${NON_INTERACTIVE:-}" != "1" ] && [ ! -t 0 ]; then
error "Interactive third-party software acceptance requires a TTY. Re-run in a terminal or pass --yes-i-accept-third-party-software (or set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1)."
fi
# Fail-fast license-acceptance check (#2671). Headless curl|bash still exits
# before phase 1 so it cannot leave a half-install behind. Piped installs from
# a real terminal are different: stdin is the script pipe, but /dev/tty can
# still collect acceptance before Node.js or the CLI are installed.
preflight_usage_notice_prompt

_INSTALL_START=$SECONDS
print_banner
Expand Down
25 changes: 24 additions & 1 deletion src/lib/commands/onboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { runOnboardAction } from "../global-cli-actions";
import OnboardCliCommand from "./onboard";
Expand All @@ -15,11 +15,34 @@ vi.mock("../global-cli-actions", () => ({
const rootDir = process.cwd();

describe("onboard oclif command", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("rejects mutually exclusive resume and fresh flags before dispatch", async () => {
await expect(OnboardCliCommand.run(["--resume", "--fresh"], rootDir)).rejects.toThrow(
/resume|fresh/,
);

expect(runOnboardAction).not.toHaveBeenCalled();
});

it("accepts --yes and forwards it to the legacy onboard action", async () => {
await OnboardCliCommand.run(
["--non-interactive", "--yes", "--yes-i-accept-third-party-software"],
rootDir,
);

expect(runOnboardAction).toHaveBeenCalledWith([
"--non-interactive",
"--yes",
"--yes-i-accept-third-party-software",
]);
});

it("accepts -y as the short form for --yes", async () => {
await OnboardCliCommand.run(["--non-interactive", "-y"], rootDir);

expect(runOnboardAction).toHaveBeenCalledWith(["--non-interactive", "--yes"]);
});
});
4 changes: 2 additions & 2 deletions src/lib/commands/onboard/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const onboardExamples = [
"<%= config.bin %> onboard --resume",
"<%= config.bin %> onboard --fresh",
"<%= config.bin %> onboard --from ./Dockerfile --name alpha",
`<%= config.bin %> onboard --non-interactive --name alpha ${NOTICE_ACCEPT_FLAG}`,
`<%= config.bin %> onboard --non-interactive --yes --name alpha ${NOTICE_ACCEPT_FLAG}`,
];

export type OnboardFlags = {
Expand Down Expand Up @@ -56,7 +56,7 @@ export function buildOnboardFlags(): Record<string, any> {
}),
yes: Flags.boolean({
char: "y",
description: "Auto-accept the Ollama model-download size confirmation",
description: "Auto-confirm prompts that are safe for unattended onboarding",
}),
[acceptFlagName]: Flags.boolean({ description: "Accept the third-party software notice" }),
} as Record<string, any>;
Expand Down
Loading
Loading