diff --git a/scripts/install.sh b/scripts/install.sh index a515cf85ddb..fc1194739f9 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3088,13 +3088,19 @@ fail_force_station_terminal_required() { } validate_force_station_install_override() { - local platform="$1" + local platform="$1" release_state if [ "${FORCE_STATION_INSTALL:-}" != "1" ]; then return 0 fi if [ "$platform" != "DGX Station" ]; then error "--force-station-install requires DGX Station GB300 hardware (detected: ${platform:-unsupported platform})." fi + release_state="$(classify_dgx_station_release)" + case "$release_state" in + generic-ubuntu | supported-dgx-os | supported-colossus-baseos | supported-ai-developer-tools) + error "--force-station-install is only for unrecognized DGX Station release metadata. This host is already supported (${release_state}); omit --force-station-install." + ;; + esac if [ "${NEMOCLAW_NO_EXPRESS:-}" = "1" ]; then error "--force-station-install cannot be combined with NEMOCLAW_NO_EXPRESS=1. Remove one override." fi @@ -3689,7 +3695,9 @@ maybe_offer_express_install() { describe_express_install "$platform" printf " Run express install with these settings? [Y/n]: " if ! IFS= read -r reply; then - if [ "${STATION_DEEPSEEK:-}" = "1" ]; then + if [ "${FORCE_STATION_INSTALL:-}" = "1" ]; then + fail_force_station_terminal_required + elif [ "${STATION_DEEPSEEK:-}" = "1" ]; then fail_station_deepseek_terminal_required fi info "Skipping express install (unable to read from TTY)." @@ -3701,7 +3709,9 @@ maybe_offer_express_install() { printf " Run express install with these settings? [Y/n]: " if ! IFS= read -r reply <&3; then exec 3<&- - if [ "${STATION_DEEPSEEK:-}" = "1" ]; then + if [ "${FORCE_STATION_INSTALL:-}" = "1" ]; then + fail_force_station_terminal_required + elif [ "${STATION_DEEPSEEK:-}" = "1" ]; then fail_station_deepseek_terminal_required fi info "Skipping express install (unable to read from TTY)." diff --git a/scripts/prepare-dgx-station-host.sh b/scripts/prepare-dgx-station-host.sh index f41b91eb3df..a392a72d9d9 100755 --- a/scripts/prepare-dgx-station-host.sh +++ b/scripts/prepare-dgx-station-host.sh @@ -631,6 +631,13 @@ check_platform() { is_station_gb300_product "$product" || fatal "Expected DGX Station GB300 DMI, found ${product}" release_path="$(dgx_station_release_path)" release_state="$(dgx_station_release_state "$release_path")" + if ((FORCE_STATION_INSTALL == 1)); then + case "$release_state" in + generic-ubuntu | supported-dgx-os | supported-colossus-baseos | supported-ai-developer-tools) + fatal "--force-station-install is only for unrecognized DGX Station release metadata. This host is already supported (${release_state}); omit --force-station-install." + ;; + esac + fi case "$release_state" in generic-ubuntu) station_has_exact_gb300_pci_gpu "$(station_pci_devices_path)" \ @@ -642,6 +649,8 @@ check_platform() { supported-ai-developer-tools) STATION_HOST_PROFILE="ai-developer-tools" ;; *) if ((FORCE_STATION_INSTALL == 1)); then + station_has_exact_gb300_pci_gpu "$(station_pci_devices_path)" \ + || fatal "Expected an NVIDIA GB300 PCI GPU (${GB300_PCI_VENDOR#0x}:${GB300_PCI_DEVICE#0x}) before forced factory-runtime validation" STATION_HOST_PROFILE="forced-factory-runtime" warn "DGX release metadata allowlist bypassed by explicit --force-station-install intent; all hardware and factory-runtime health checks remain required" else diff --git a/test/install-express-prompt.test.ts b/test/install-express-prompt.test.ts index 2f6965760b9..b41e78e5fa3 100644 --- a/test/install-express-prompt.test.ts +++ b/test/install-express-prompt.test.ts @@ -59,6 +59,7 @@ else: script = r''' source "$INSTALLER_UNDER_TEST" >/dev/null detect_express_platform() { printf "$EXPRESS_PLATFORM"; } +classify_dgx_station_release() { printf "%s" "\${EXPRESS_RELEASE_STATE:-generic-ubuntu}"; } NON_INTERACTIVE="\${NON_INTERACTIVE:-}" NEMOCLAW_PROVIDER="\${NEMOCLAW_PROVIDER:-}" NEMOCLAW_NO_EXPRESS="\${NEMOCLAW_NO_EXPRESS:-}" @@ -519,10 +520,24 @@ ensure_station_express_host`, name: "a forced Station install in non-interactive mode", args: ["--force-station-install", "--non-interactive"], platform: "DGX Station", - env: {}, + env: { EXPRESS_RELEASE_STATE: "unsupported-dgx-os" }, message: /--force-station-install selects the DGX Station express prompt and cannot be combined with non-interactive mode \(triggered by: the --non-interactive flag\)/, }, + ...[ + "generic-ubuntu", + "supported-dgx-os", + "supported-colossus-baseos", + "supported-ai-developer-tools", + ].map((releaseState) => ({ + name: `an unnecessary forced Station install on ${releaseState}`, + args: ["--force-station-install"], + platform: "DGX Station", + env: { EXPRESS_RELEASE_STATE: releaseState }, + message: new RegExp( + `This host is already supported \\(${releaseState}\\); omit --force-station-install`, + ), + })), { name: "a Station-only flag on DGX Spark", args: ["--station-deepseek"], @@ -570,6 +585,7 @@ ensure_station_express_host`, ` source "$INSTALLER_UNDER_TEST" >/dev/null detect_express_platform() { printf "%s" "$EXPRESS_PLATFORM"; } +classify_dgx_station_release() { printf "%s" "\${EXPRESS_RELEASE_STATE:-generic-ubuntu}"; } ensure_docker() { printf "ensure_docker\\n" >>"$MUTATION_LOG"; } ensure_openshell_build_deps() { printf "ensure_openshell_build_deps\\n" >>"$MUTATION_LOG"; } main "$@" @@ -762,6 +778,20 @@ sys.exit(result.returncode) expect(output).not.toMatch(/RESULT NON_INTERACTIVE=/); }); + it("fails closed if the forced Station prompt becomes unreadable after preflight (#7138)", () => { + const result = runExpressPromptWithTty("", "tty", "DGX Station", { + EXPRESS_RELEASE_STATE: "unsupported-dgx-os", + FORCE_EXPRESS_PROMPT_READ_FAILURE: "1", + FORCE_STATION_INSTALL: "1", + }); + const output = `${result.stdout}${result.stderr}`; + expect(result.error, output).toBeUndefined(); + expect(result.status, output).not.toBe(0); + expect(output).toMatch(/--force-station-install.*needs an interactive terminal/); + expect(output).not.toMatch(/Skipping express install/); + expect(output).not.toMatch(/RESULT NON_INTERACTIVE=/); + }); + it.each([ ["Unsupported DGX Station OS", { NEMOCLAW_NO_EXPRESS: "1" }], ["Unsupported DGX Station generation", { NEMOCLAW_PROVIDER: "openai" }], diff --git a/test/install-station-dgx-os.test.ts b/test/install-station-dgx-os.test.ts index 8b31d3c6655..bffe5d85572 100644 --- a/test/install-station-dgx-os.test.ts +++ b/test/install-station-dgx-os.test.ts @@ -403,6 +403,7 @@ station_os_release_path() { printf '%s' "$HOME/os-release"; } station_product_name_path() { printf '%s' "$HOME/product-name"; } dgx_station_release_path() { printf '%s' "$HOME/dgx-release"; } dgx_station_release_state() { printf 'unsupported-dgx-os'; } +station_has_exact_gb300_pci_gpu() { return 0; } FORCE_STATION_INSTALL=1 check_platform printf 'PROFILE=%s\n' "$STATION_HOST_PROFILE" @@ -431,6 +432,57 @@ check_platform expect(unforced.output).toContain("outside the validated boundary"); }); + it.each([ + "generic-ubuntu", + "supported-dgx-os", + "supported-colossus-baseos", + "supported-ai-developer-tools", + ])("rejects explicit metadata intent for recognized %s before preparation (#7138)", (releaseState) => { + const { result, output } = runSourced( + STATION_PREPARE, + ` +printf 'ID=ubuntu\nVERSION_ID="24.04"\nPRETTY_NAME="Ubuntu 24.04"\n' >"$HOME/os-release" +printf 'NVIDIA DGX Station GB300\n' >"$HOME/product-name" +uname() { printf 'aarch64\n'; } +station_os_release_path() { printf '%s' "$HOME/os-release"; } +station_product_name_path() { printf '%s' "$HOME/product-name"; } +dgx_station_release_path() { printf '%s' "$HOME/dgx-release"; } +dgx_station_release_state() { printf '%s' "$RELEASE_STATE"; } +FORCE_STATION_INSTALL=1 +check_platform +printf 'PREPARATION_REACHED\n' +`, + { RELEASE_STATE: releaseState }, + ); + + expect(result.status, output).not.toBe(0); + expect(output).toContain( + `This host is already supported (${releaseState}); omit --force-station-install`, + ); + expect(output).not.toContain("PREPARATION_REACHED"); + }); + + it("keeps generic Ubuntu preparation unchanged without explicit metadata intent (#7138)", () => { + const { result, output } = runSourced( + STATION_PREPARE, + ` +printf 'ID=ubuntu\nVERSION_ID="24.04"\nPRETTY_NAME="Ubuntu 24.04"\n' >"$HOME/os-release" +printf 'NVIDIA DGX Station GB300\n' >"$HOME/product-name" +uname() { printf 'aarch64\n'; } +station_os_release_path() { printf '%s' "$HOME/os-release"; } +station_product_name_path() { printf '%s' "$HOME/product-name"; } +dgx_station_release_path() { printf '%s' "$HOME/dgx-release"; } +dgx_station_release_state() { printf 'generic-ubuntu'; } +station_has_exact_gb300_pci_gpu() { return 0; } +check_platform +printf 'PROFILE=%s\n' "$STATION_HOST_PROFILE" +`, + ); + + expect(result.status, output).toBe(0); + expect(output).toContain("PROFILE=generic-ubuntu"); + }); + it("parses the metadata override only alongside a preparation mode", () => { const accepted = runSourced( STATION_PREPARE, diff --git a/test/install-station-platform-identity.test.ts b/test/install-station-platform-identity.test.ts index f904a4bfbcb..fb46862f011 100644 --- a/test/install-station-platform-identity.test.ts +++ b/test/install-station-platform-identity.test.ts @@ -135,4 +135,50 @@ run_apply expect(output).toContain("Expected an NVIDIA GB300 PCI GPU (10de:31c2)"); expect(output).not.toContain("UNEXPECTED_MUTATION"); }); + + it("rejects forced metadata intent without the exact GB300 PCI identity before mutation (#7138)", () => { + const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-station-platform-")); + const osReleasePath = path.join(fixtureRoot, "os-release"); + const productNamePath = path.join(fixtureRoot, "product_name"); + const dgxReleasePath = path.join(fixtureRoot, "dgx-release"); + const pciRoot = writePciIdentityFixture("0x1234"); + fs.writeFileSync( + osReleasePath, + 'ID=ubuntu\nVERSION_ID="24.04"\nPRETTY_NAME="Ubuntu 24.04.4 LTS"\n', + ); + fs.writeFileSync(productNamePath, "DGX Station GB300\n"); + fs.writeFileSync(dgxReleasePath, 'DGX_PRETTY_NAME="Unrecognized Station"\n'); + + const { result, output } = runStationPrepare( + ` +station_os_release_path() { printf '%s' "$OS_RELEASE_PATH"; } +station_product_name_path() { printf '%s' "$PRODUCT_NAME_PATH"; } +station_pci_devices_path() { printf '%s' "$PCI_ROOT"; } +dgx_station_release_path() { printf '%s' "$DGX_RELEASE_PATH"; } +dgx_station_release_state() { printf 'unsupported-dgx-os'; } +uname() { + case "$*" in + -m) printf 'aarch64' ;; + -r) printf 'test-kernel' ;; + *) return 1 ;; + esac +} +require_command() { :; } +acquire_sudo() { :; } +install_packages() { printf 'UNEXPECTED_MUTATION\n'; } +FORCE_STATION_INSTALL=1 +run_apply +`, + { + OS_RELEASE_PATH: osReleasePath, + PRODUCT_NAME_PATH: productNamePath, + PCI_ROOT: pciRoot, + DGX_RELEASE_PATH: dgxReleasePath, + }, + ); + + expect(result.status, output).not.toBe(0); + expect(output).toContain("Expected an NVIDIA GB300 PCI GPU (10de:31c2)"); + expect(output).not.toContain("UNEXPECTED_MUTATION"); + }); });