From d84f8392d5d0af97c5514089a1b2fa58af3e9499 Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 08:34:00 -0700 Subject: [PATCH 01/12] fix(onboard): continue onboarding when Homebrew cannot confirm the OpenShell formula Homebrew 6.x refuses to load formulae from taps it has not marked trusted, so brew info fails for the pinned nvidia/openshell tap and the formula identity check aborted onboarding before preflight. Treat an unconfirmed identity as no managed Homebrew service: warn once with brew's own reason and continue on the standalone gateway. Positive evidence of a missing or wrong-tap formula still fails closed. Refs: #7707 Signed-off-by: harjoth --- .../docker-driver-gateway-service.test.ts | 18 +++++ .../onboard/docker-driver-gateway-service.ts | 20 +++++- ...ost-runtime-homebrew-untrusted-tap.test.ts | 66 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index 85a56eb2170..00ab18cddd6 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -146,6 +146,24 @@ describe("docker-driver-gateway-service", () => { ).toThrow("must come from nvidia/openshell"); }); + it("continues without the Homebrew service when brew refuses to load the formula (#7707)", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const refusal = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + const options = { + commandExists: () => true, + platform: "darwin" as NodeJS.Platform, + spawnSyncImpl: vi.fn((_command: string, args: string[]) => + args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), + ), + }; + + expect(hasOpenShellGatewayUserService(options)).toBe(false); + expect(hasOpenShellGatewayUserService(options)).toBe(false); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith(expect.stringContaining(refusal)); + }); + it("rejects a missing Homebrew formula when Homebrew is available (#6903)", () => { expect(() => hasOpenShellGatewayUserService({ diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index 78e47ce04df..7a371ed3f1e 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -239,6 +239,21 @@ function hasUpstreamOpenShellGatewayUserService( return getOpenShellGatewayUserServicePaths().some(existsSync); } +const warnedHomebrewIdentityCheckReasons = new Set(); + +// Homebrew can refuse to load the pinned formula (for example when Homebrew +// 6.x marks the nvidia/openshell tap untrusted), which leaves the identity +// unconfirmed without being evidence of a wrong formula. Managing the service +// through brew would fail the same way, so continue on the standalone gateway +// instead of aborting (#7707). +function warnHomebrewIdentityCheckUnavailable(reason: string): void { + if (warnedHomebrewIdentityCheckReasons.has(reason)) return; + warnedHomebrewIdentityCheckReasons.add(reason); + console.warn( + ` Homebrew could not confirm the OpenShell formula identity; continuing without the Homebrew-managed gateway service.\n ${reason}`, + ); +} + function hasOfficialHomebrewFormula( opts: Pick< OpenShellGatewayUserServiceOptions, @@ -259,7 +274,10 @@ function hasOfficialHomebrewFormula( env, spawnSyncImpl, }); - if (!info.ok) throw new Error(`OpenShell Homebrew formula identity check failed: ${info.reason}`); + if (!info.ok) { + warnHomebrewIdentityCheckUnavailable(info.reason ?? "brew info failed"); + return false; + } try { const parsed = JSON.parse(info.stdout ?? "") as { formulae?: Array<{ name?: string; tap?: string }>; diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts new file mode 100644 index 00000000000..e134dceb4a0 --- /dev/null +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { createGatewayHostRuntime, type GatewayHostRuntimeDeps } from "./gateway-host-runtime"; +import type { PortProbeResult } from "./preflight"; + +// Homebrew 6.x refuses to load formulae from taps it has not marked trusted, +// so `brew info --json=v2 openshell` fails even though the formula is the +// pinned official one. The refusal reaches the gateway-owner resolution +// through the real spawnSync path, not an injected seam (#7707). +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawnSync: vi.fn((command: string, args: readonly string[]) => + command === "brew" && args[0] === "info" + ? { + status: 1, + stderr: + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.", + stdout: "", + } + : { status: 0, stderr: "", stdout: "" }, + ), + }; +}); + +const ORIGINAL_ENV = { ...process.env }; + +beforeEach(() => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + vi.spyOn(console, "warn").mockImplementation(() => {}); +}); + +afterEach(() => { + process.env = { ...ORIGINAL_ENV }; + vi.restoreAllMocks(); +}); + +function createDeps(): GatewayHostRuntimeDeps { + return { + applyOverlayfsAutoFix: () => null, + checkGatewayPortAvailable: async () => ({ ok: true }) as PortProbeResult, + gatewayName: () => "nemoclaw", + gatewayPort: () => 8080, + getGatewayPortListenerRawScan: () => ({ pids: [], complete: true }), + getInstalledOpenshellVersion: () => "0.0.85", + runCaptureOpenshell: () => "healthy", + runOpenshell: () => ({ status: 0 }), + resolveOpenShellGatewayBinary: () => null, + waitForGatewayHttpReady: async () => true, + }; +} + +describe("gateway host runtime on Homebrew 6.x untrusted tap", () => { + it("resolves a standalone owner instead of aborting when brew refuses the pinned tap (#7707)", () => { + expect(createGatewayHostRuntime(createDeps()).getGatewayOwner()).toMatchObject({ + gatewayName: "nemoclaw", + gatewayPort: 8080, + mode: "nemoclaw-managed", + source: "standalone", + }); + }); +}); From 8350421dce4eb8fcc8cb991964641af65dadff0f Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 08:39:26 -0700 Subject: [PATCH 02/12] docs(reference): document the standalone fallback for unconfirmed Homebrew formula identity Refs: #7707 Signed-off-by: harjoth --- ci/platform-matrix.json | 2 +- docs/get-started/prerequisites.mdx | 2 +- docs/reference/architecture.mdx | 3 ++- docs/reference/platform-support.mdx | 2 +- docs/reference/troubleshooting.mdx | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 2e27ab585ab..79dc69593a9 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -39,7 +39,7 @@ "status": "caveated", "prd_priority": "P0", "ci_tested": true, - "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." + "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew cannot confirm the formula identity, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." }, { "name": "DGX OS (Spark)", diff --git a/docs/get-started/prerequisites.mdx b/docs/get-started/prerequisites.mdx index 41859d63a6d..9afbb00112d 100644 --- a/docs/get-started/prerequisites.mdx +++ b/docs/get-started/prerequisites.mdx @@ -96,7 +96,7 @@ The table comes from [`ci/platform-matrix.json`](https://github.com/NVIDIA/NemoC | DGX OS (Spark) | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | Tested with limitations across qualified profiles on one physical DGX Station GB300; see [Additional Setup for DGX Station](additional-setup/dgx-station-preparation) for accepted profiles, the pending no-OTA DGX OS `7.6.x` end-to-end qualification, runtime gates, and current dual-Station and dedicated CI limitations. | | Linux | Docker | Tested | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew cannot confirm the formula identity, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | Requires WSL2 with Docker Desktop backend. See [Additional Setup for Windows Machines](additional-setup/windows-preparation) before the Quickstart. | For the complete platform support matrix, including all deferred platforms and CI coverage, refer to [Platform Support](../reference/platform-support). diff --git a/docs/reference/architecture.mdx b/docs/reference/architecture.mdx index 49f7a29cc5c..d5e52fb7f5b 100644 --- a/docs/reference/architecture.mdx +++ b/docs/reference/architecture.mdx @@ -90,7 +90,8 @@ The standalone Linux process is used only when the systemd user manager is unava On Apple Silicon macOS, Homebrew makes the official OpenShell formula authoritative. The installer stages the formula and onboarding starts its `openshell` service. When Homebrew is present, a missing formula, a formula from another tap, a service-start failure, or a health failure stops onboarding. -Only a host without Homebrew uses the standalone macOS gateway fallback. +When Homebrew cannot confirm the formula identity, for example when it refuses to load the formula from a tap it has not marked trusted, onboarding warns and uses the standalone macOS gateway fallback. +A host without Homebrew also uses the standalone macOS gateway fallback. NemoClaw-managed gateways on custom ports remain detached and separate from the default service. An externally supervised gateway can use any matching configured port; its declared supervisor retains lifecycle authority. In both Docker-driver modes, the sandbox is a Docker container, not a Kubernetes pod. diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index e958d24f70d..84447d3db78 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -81,7 +81,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Spark) | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew cannot confirm the formula identity, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. | {/* platform-matrix-full:end */} diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 789b2337f3a..8a440b91065 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -911,7 +911,7 @@ Follow these steps to reconnect. ``` If Homebrew is present but `openshell` is missing or comes from another tap, rerun the NemoClaw installer. - Onboarding does not use the standalone fallback while Homebrew is present. + If Homebrew refuses to load the formula and cannot confirm its identity, onboarding warns with Homebrew's reason and continues on the standalone fallback. On Linux package installs, inspect and restart the upstream service. From cb9d771c4cb037ec8d1fd521868132b440efa990 Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 08:47:01 -0700 Subject: [PATCH 03/12] fix(onboard): limit the Homebrew fallback to the pinned-tap load refusal Cross-review found the fallback too broad: any brew info failure skipped the identity gate, and the standalone path can still execute a brew-installed binary. Only the refusal that names the pinned nvidia/openshell formula and tap now degrades to the standalone fallback; every other failure keeps the fail-closed abort. Refs: #7707 Signed-off-by: harjoth --- docs/reference/architecture.mdx | 3 +- .../docker-driver-gateway-service.test.ts | 53 +++++++++++++++++++ .../onboard/docker-driver-gateway-service.ts | 28 +++++++--- ...ost-runtime-homebrew-untrusted-tap.test.ts | 7 ++- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/docs/reference/architecture.mdx b/docs/reference/architecture.mdx index d5e52fb7f5b..822c30526d4 100644 --- a/docs/reference/architecture.mdx +++ b/docs/reference/architecture.mdx @@ -90,7 +90,8 @@ The standalone Linux process is used only when the systemd user manager is unava On Apple Silicon macOS, Homebrew makes the official OpenShell formula authoritative. The installer stages the formula and onboarding starts its `openshell` service. When Homebrew is present, a missing formula, a formula from another tap, a service-start failure, or a health failure stops onboarding. -When Homebrew cannot confirm the formula identity, for example when it refuses to load the formula from a tap it has not marked trusted, onboarding warns and uses the standalone macOS gateway fallback. +Homebrew 6.x refuses to load formulae from taps it has not marked trusted, so it can fail to confirm the official formula's identity. +When that happens, onboarding warns and uses the standalone macOS gateway fallback. A host without Homebrew also uses the standalone macOS gateway fallback. NemoClaw-managed gateways on custom ports remain detached and separate from the default service. An externally supervised gateway can use any matching configured port; its declared supervisor retains lifecycle authority. diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index 00ab18cddd6..f61e7737173 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -164,6 +164,59 @@ describe("docker-driver-gateway-service", () => { expect(warn).toHaveBeenCalledWith(expect.stringContaining(refusal)); }); + it.each([ + ["a generic brew info failure", "Error: Permission denied"], + [ + "a refused foreign-tap formula", + "Error: Refusing to load formula other/tap/openshell from untrusted tap other/tap.", + ], + ])("keeps %s fatal during the formula identity check (#7707)", (_case, reason) => { + expect(() => + hasOpenShellGatewayUserService({ + commandExists: () => true, + platform: "darwin", + spawnSyncImpl: vi.fn((_command: string, args: string[]) => + args[0] === "info" ? spawnResult(1, reason) : spawnResult(), + ), + }), + ).toThrow(`OpenShell Homebrew formula identity check failed: ${reason}`); + }); + + it("still reports a missing formula when brew list is blocked by the untrusted-tap refusal (#7707)", () => { + const refusal = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + expect(() => + hasOpenShellGatewayUserService({ + commandExists: () => true, + platform: "darwin", + spawnSyncImpl: () => spawnResult(1, refusal), + }), + ).toThrow("official OpenShell Homebrew formula is not installed"); + }); + + it("skips the Homebrew-managed start when the formula identity is unconfirmed (#7707)", async () => { + const refusal = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + const started = await startPackageManagedDockerDriverGateway({ + clearDockerDriverGatewayRuntimeFiles: () => {}, + exitOnFailure: false, + gatewayName: "nemoclaw", + hasOpenShellGatewayUserService: () => + hasOpenShellGatewayUserService({ + commandExists: () => true, + platform: "darwin", + spawnSyncImpl: (_command: string, args: string[]) => + args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), + }), + registerDockerDriverGatewayEndpoint: () => true, + runCaptureOpenshell: () => "", + skipSandboxBridgeReachability: true, + verifySandboxBridgeGatewayReachableOrExit: async () => {}, + }); + + expect(started).toBe(false); + }); + it("rejects a missing Homebrew formula when Homebrew is available (#6903)", () => { expect(() => hasOpenShellGatewayUserService({ diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index 7a371ed3f1e..f47fcaaffac 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -241,16 +241,24 @@ function hasUpstreamOpenShellGatewayUserService( const warnedHomebrewIdentityCheckReasons = new Set(); -// Homebrew can refuse to load the pinned formula (for example when Homebrew -// 6.x marks the nvidia/openshell tap untrusted), which leaves the identity -// unconfirmed without being evidence of a wrong formula. Managing the service -// through brew would fail the same way, so continue on the standalone gateway -// instead of aborting (#7707). +// Homebrew 6.x refuses to load formulae from taps it has not marked trusted. +// For the pinned official formula that refusal names the right tap, so it is +// not evidence of a wrong formula, and managing the service through brew would +// fail the same way. Continue on the standalone gateway fallback instead of +// aborting; any other brew failure keeps the fail-closed abort (#7707). +function isPinnedTapLoadRefusal(reason: string): boolean { + return reason + .replace(/\s+/g, " ") + .includes( + `Refusing to load formula ${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} from untrusted tap ${OPENSHELL_GATEWAY_HOMEBREW_TAP}`, + ); +} + function warnHomebrewIdentityCheckUnavailable(reason: string): void { if (warnedHomebrewIdentityCheckReasons.has(reason)) return; warnedHomebrewIdentityCheckReasons.add(reason); console.warn( - ` Homebrew could not confirm the OpenShell formula identity; continuing without the Homebrew-managed gateway service.\n ${reason}`, + ` Homebrew could not confirm the OpenShell formula identity; continuing on the standalone gateway fallback.\n ${reason}`, ); } @@ -275,8 +283,12 @@ function hasOfficialHomebrewFormula( spawnSyncImpl, }); if (!info.ok) { - warnHomebrewIdentityCheckUnavailable(info.reason ?? "brew info failed"); - return false; + const reason = info.reason ?? "brew info failed"; + if (isPinnedTapLoadRefusal(reason)) { + warnHomebrewIdentityCheckUnavailable(reason); + return false; + } + throw new Error(`OpenShell Homebrew formula identity check failed: ${reason}`); } try { const parsed = JSON.parse(info.stdout ?? "") as { diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts index e134dceb4a0..647c8548bb9 100644 --- a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -56,11 +56,16 @@ function createDeps(): GatewayHostRuntimeDeps { describe("gateway host runtime on Homebrew 6.x untrusted tap", () => { it("resolves a standalone owner instead of aborting when brew refuses the pinned tap (#7707)", () => { - expect(createGatewayHostRuntime(createDeps()).getGatewayOwner()).toMatchObject({ + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const runtime = createGatewayHostRuntime(createDeps()); + + expect(runtime.getGatewayOwner()).toMatchObject({ gatewayName: "nemoclaw", gatewayPort: 8080, mode: "nemoclaw-managed", source: "standalone", }); + expect(runtime.getGatewayOwner()).toMatchObject({ source: "standalone" }); + expect(warn).toHaveBeenCalledTimes(1); }); }); From d37a028087dfaf231acdaffe600dd9dbc8ebb183 Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 08:51:31 -0700 Subject: [PATCH 04/12] fix(onboard): name the pinned-tap refusal precisely in the fallback warning and docs Refs: #7707 Signed-off-by: harjoth --- ci/platform-matrix.json | 2 +- docs/get-started/prerequisites.mdx | 2 +- docs/reference/platform-support.mdx | 2 +- src/lib/onboard/docker-driver-gateway-service.ts | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 79dc69593a9..4fc3e0bc557 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -39,7 +39,7 @@ "status": "caveated", "prd_priority": "P0", "ci_tested": true, - "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew cannot confirm the formula identity, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." + "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." }, { "name": "DGX OS (Spark)", diff --git a/docs/get-started/prerequisites.mdx b/docs/get-started/prerequisites.mdx index 9afbb00112d..169a3bbba1a 100644 --- a/docs/get-started/prerequisites.mdx +++ b/docs/get-started/prerequisites.mdx @@ -96,7 +96,7 @@ The table comes from [`ci/platform-matrix.json`](https://github.com/NVIDIA/NemoC | DGX OS (Spark) | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | Tested with limitations across qualified profiles on one physical DGX Station GB300; see [Additional Setup for DGX Station](additional-setup/dgx-station-preparation) for accepted profiles, the pending no-OTA DGX OS `7.6.x` end-to-end qualification, runtime gates, and current dual-Station and dedicated CI limitations. | | Linux | Docker | Tested | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew cannot confirm the formula identity, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | Requires WSL2 with Docker Desktop backend. See [Additional Setup for Windows Machines](additional-setup/windows-preparation) before the Quickstart. | For the complete platform support matrix, including all deferred platforms and CI coverage, refer to [Platform Support](../reference/platform-support). diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index 84447d3db78..b8e9306ebe5 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -81,7 +81,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Spark) | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew cannot confirm the formula identity, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. | {/* platform-matrix-full:end */} diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index f47fcaaffac..b4a3a2ccff3 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -246,6 +246,8 @@ const warnedHomebrewIdentityCheckReasons = new Set(); // not evidence of a wrong formula, and managing the service through brew would // fail the same way. Continue on the standalone gateway fallback instead of // aborting; any other brew failure keeps the fail-closed abort (#7707). +// The match is against Homebrew's literal refusal text: if Homebrew rewords +// it, this case reverts to the fail-closed abort, not to a bypass. function isPinnedTapLoadRefusal(reason: string): boolean { return reason .replace(/\s+/g, " ") @@ -258,7 +260,7 @@ function warnHomebrewIdentityCheckUnavailable(reason: string): void { if (warnedHomebrewIdentityCheckReasons.has(reason)) return; warnedHomebrewIdentityCheckReasons.add(reason); console.warn( - ` Homebrew could not confirm the OpenShell formula identity; continuing on the standalone gateway fallback.\n ${reason}`, + ` Homebrew could not confirm the OpenShell formula identity; falling back to the standalone gateway.\n ${reason}`, ); } From 3b75153ebbf12d9c68b85de001a002b94e6e0235 Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 09:20:48 -0700 Subject: [PATCH 05/12] fix(onboard): keep a loaded Homebrew launchd service fatal during tap-refusal fallback Review found the fallback made the standalone cutover path reachable while launchd still owned a loaded openshell service: cutover could adopt that process or kill one launchd would restart. Probe launchctl (which does not load the formula) before degrading and abort with stop-the-service guidance while the unit is loaded. Also bound the refusal match so taps that only start with the pinned name stay fatal, and assert the managed start is never invoked in the fallback test. Refs: #7707 Signed-off-by: harjoth --- ci/platform-matrix.json | 2 +- docs/get-started/prerequisites.mdx | 2 +- docs/reference/architecture.mdx | 3 +- docs/reference/platform-support.mdx | 2 +- docs/reference/troubleshooting.mdx | 1 + .../docker-driver-gateway-service.test.ts | 43 +++++++++++++++++-- .../onboard/docker-driver-gateway-service.ts | 43 ++++++++++++++----- ...ost-runtime-homebrew-untrusted-tap.test.ts | 4 +- 8 files changed, 81 insertions(+), 19 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 4fc3e0bc557..4db00197df8 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -39,7 +39,7 @@ "status": "caveated", "prd_priority": "P0", "ci_tested": true, - "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." + "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and no openshell launchd service is loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." }, { "name": "DGX OS (Spark)", diff --git a/docs/get-started/prerequisites.mdx b/docs/get-started/prerequisites.mdx index 169a3bbba1a..2c78be04a29 100644 --- a/docs/get-started/prerequisites.mdx +++ b/docs/get-started/prerequisites.mdx @@ -96,7 +96,7 @@ The table comes from [`ci/platform-matrix.json`](https://github.com/NVIDIA/NemoC | DGX OS (Spark) | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | Tested with limitations across qualified profiles on one physical DGX Station GB300; see [Additional Setup for DGX Station](additional-setup/dgx-station-preparation) for accepted profiles, the pending no-OTA DGX OS `7.6.x` end-to-end qualification, runtime gates, and current dual-Station and dedicated CI limitations. | | Linux | Docker | Tested | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and no openshell launchd service is loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | Requires WSL2 with Docker Desktop backend. See [Additional Setup for Windows Machines](additional-setup/windows-preparation) before the Quickstart. | For the complete platform support matrix, including all deferred platforms and CI coverage, refer to [Platform Support](../reference/platform-support). diff --git a/docs/reference/architecture.mdx b/docs/reference/architecture.mdx index 822c30526d4..caab09a7b99 100644 --- a/docs/reference/architecture.mdx +++ b/docs/reference/architecture.mdx @@ -91,7 +91,8 @@ On Apple Silicon macOS, Homebrew makes the official OpenShell formula authoritat The installer stages the formula and onboarding starts its `openshell` service. When Homebrew is present, a missing formula, a formula from another tap, a service-start failure, or a health failure stops onboarding. Homebrew 6.x refuses to load formulae from taps it has not marked trusted, so it can fail to confirm the official formula's identity. -When that happens, onboarding warns and uses the standalone macOS gateway fallback. +When that happens and no `openshell` launchd service is loaded, onboarding warns and uses the standalone macOS gateway fallback. +A loaded launchd service keeps its lifecycle authority, so onboarding stops until the service is stopped. A host without Homebrew also uses the standalone macOS gateway fallback. NemoClaw-managed gateways on custom ports remain detached and separate from the default service. An externally supervised gateway can use any matching configured port; its declared supervisor retains lifecycle authority. diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index b8e9306ebe5..79d2a948f13 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -81,7 +81,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Spark) | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and no openshell launchd service is loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. | {/* platform-matrix-full:end */} diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 8a440b91065..094eba7c731 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -912,6 +912,7 @@ Follow these steps to reconnect. If Homebrew is present but `openshell` is missing or comes from another tap, rerun the NemoClaw installer. If Homebrew refuses to load the formula and cannot confirm its identity, onboarding warns with Homebrew's reason and continues on the standalone fallback. + If the `homebrew.mxcl.openshell` launchd service is still loaded when that happens, onboarding stops instead and asks you to stop the service first, because NemoClaw cannot manage or safely replace a service launchd owns. On Linux package installs, inspect and restart the upstream service. diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index f61e7737173..61e557e78d0 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -153,8 +153,12 @@ describe("docker-driver-gateway-service", () => { const options = { commandExists: () => true, platform: "darwin" as NodeJS.Platform, - spawnSyncImpl: vi.fn((_command: string, args: string[]) => - args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), + spawnSyncImpl: vi.fn((command: string, args: string[]) => + command === "launchctl" + ? spawnResult(1, "Could not find service") + : args[0] === "info" + ? spawnResult(1, refusal) + : spawnResult(), ), }; @@ -164,12 +168,34 @@ describe("docker-driver-gateway-service", () => { expect(warn).toHaveBeenCalledWith(expect.stringContaining(refusal)); }); + it("aborts instead of falling back while the Homebrew launchd service is loaded (#7707)", () => { + const refusal = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + expect(() => + hasOpenShellGatewayUserService({ + commandExists: () => true, + platform: "darwin", + spawnSyncImpl: vi.fn((command: string, args: string[]) => + command === "launchctl" + ? spawnResult(0, "", '{"PID" = 4242;}') + : args[0] === "info" + ? spawnResult(1, refusal) + : spawnResult(), + ), + }), + ).toThrow("launchd service homebrew.mxcl.openshell is loaded"); + }); + it.each([ ["a generic brew info failure", "Error: Permission denied"], [ "a refused foreign-tap formula", "Error: Refusing to load formula other/tap/openshell from untrusted tap other/tap.", ], + [ + "a refusal naming a tap that only starts with the pinned name", + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell-fork.", + ], ])("keeps %s fatal during the formula identity check (#7707)", (_case, reason) => { expect(() => hasOpenShellGatewayUserService({ @@ -197,6 +223,9 @@ describe("docker-driver-gateway-service", () => { it("skips the Homebrew-managed start when the formula identity is unconfirmed (#7707)", async () => { const refusal = "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + const startService = vi.fn(() => { + throw new Error("managed start must not run"); + }); const started = await startPackageManagedDockerDriverGateway({ clearDockerDriverGatewayRuntimeFiles: () => {}, exitOnFailure: false, @@ -205,16 +234,22 @@ describe("docker-driver-gateway-service", () => { hasOpenShellGatewayUserService({ commandExists: () => true, platform: "darwin", - spawnSyncImpl: (_command: string, args: string[]) => - args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), + spawnSyncImpl: (command: string, args: string[]) => + command === "launchctl" + ? spawnResult(1, "Could not find service") + : args[0] === "info" + ? spawnResult(1, refusal) + : spawnResult(), }), registerDockerDriverGatewayEndpoint: () => true, runCaptureOpenshell: () => "", skipSandboxBridgeReachability: true, + startOpenShellGatewayUserService: startService, verifySandboxBridgeGatewayReachableOrExit: async () => {}, }); expect(started).toBe(false); + expect(startService).not.toHaveBeenCalled(); }); it("rejects a missing Homebrew formula when Homebrew is available (#6903)", () => { diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index b4a3a2ccff3..fe019497e60 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -247,13 +247,29 @@ const warnedHomebrewIdentityCheckReasons = new Set(); // fail the same way. Continue on the standalone gateway fallback instead of // aborting; any other brew failure keeps the fail-closed abort (#7707). // The match is against Homebrew's literal refusal text: if Homebrew rewords -// it, this case reverts to the fail-closed abort, not to a bypass. +// it, this case reverts to the fail-closed abort, not to a bypass. Remove this +// branch when supported Homebrew versions can verify the pinned formula +// identity again. +const PINNED_TAP_LOAD_REFUSAL_PATTERN = new RegExp( + `Refusing to load formula ${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} from untrusted tap ${OPENSHELL_GATEWAY_HOMEBREW_TAP}(?![\\w/-])`, +); + function isPinnedTapLoadRefusal(reason: string): boolean { - return reason - .replace(/\s+/g, " ") - .includes( - `Refusing to load formula ${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} from untrusted tap ${OPENSHELL_GATEWAY_HOMEBREW_TAP}`, - ); + return PINNED_TAP_LOAD_REFUSAL_PATTERN.test(reason.replace(/\s+/g, " ")); +} + +// A loaded launchd unit means launchd still owns the service lifecycle even +// when Homebrew refuses to load the formula: the standalone cutover path could +// adopt that process or kill one launchd would restart. launchctl answers +// without loading the formula, so probe it before degrading. +function isHomebrewGatewayLaunchdUnitLoaded( + opts: Required>, +): boolean { + return runCommand( + "launchctl", + ["list", `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`], + opts, + ).ok; } function warnHomebrewIdentityCheckUnavailable(reason: string): void { @@ -286,11 +302,18 @@ function hasOfficialHomebrewFormula( }); if (!info.ok) { const reason = info.reason ?? "brew info failed"; - if (isPinnedTapLoadRefusal(reason)) { - warnHomebrewIdentityCheckUnavailable(reason); - return false; + if (!isPinnedTapLoadRefusal(reason)) { + throw new Error(`OpenShell Homebrew formula identity check failed: ${reason}`); + } + if (isHomebrewGatewayLaunchdUnitLoaded({ env, spawnSyncImpl })) { + throw new Error( + `Homebrew refused to load the pinned OpenShell formula while its launchd service homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} is loaded. ` + + `NemoClaw cannot manage or safely replace that service. Stop it (launchctl bootout gui//homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}) and rerun onboarding. ` + + `Homebrew reported: ${reason}`, + ); } - throw new Error(`OpenShell Homebrew formula identity check failed: ${reason}`); + warnHomebrewIdentityCheckUnavailable(reason); + return false; } try { const parsed = JSON.parse(info.stdout ?? "") as { diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts index 647c8548bb9..3faed79087f 100644 --- a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -22,7 +22,9 @@ vi.mock("node:child_process", async (importOriginal) => { "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.", stdout: "", } - : { status: 0, stderr: "", stdout: "" }, + : command === "launchctl" + ? { status: 1, stderr: "Could not find service", stdout: "" } + : { status: 0, stderr: "", stdout: "" }, ), }; }); From ab870916c3f90bb0078ce0a8364da22171983529 Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 09:44:28 -0700 Subject: [PATCH 06/12] fix(onboard): fail closed when the launchd probe cannot answer Review found two fail-open edges in the tap-refusal fallback. The launchd probe treated a launchctl that could not run as proof the service was stopped; only a completed run that reports the unit missing now permits the fallback. The refusal matcher compared a prefix, so a neighbouring tap such as nvidia/openshell.fork still matched; it now compares the named formula and tap exactly. Refs: #7707 Signed-off-by: harjoth --- ci/platform-matrix.json | 2 +- docs/get-started/prerequisites.mdx | 2 +- docs/reference/architecture.mdx | 3 +- docs/reference/platform-support.mdx | 2 +- docs/reference/troubleshooting.mdx | 1 + .../docker-driver-gateway-service.test.ts | 31 +++++++++++- .../onboard/docker-driver-gateway-service.ts | 48 ++++++++++++++----- 7 files changed, 71 insertions(+), 18 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 4db00197df8..7f7928c9ce3 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -39,7 +39,7 @@ "status": "caveated", "prd_priority": "P0", "ci_tested": true, - "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and no openshell launchd service is loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." + "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and launchctl reports the openshell launchd service is not loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." }, { "name": "DGX OS (Spark)", diff --git a/docs/get-started/prerequisites.mdx b/docs/get-started/prerequisites.mdx index 2c78be04a29..9be307e4752 100644 --- a/docs/get-started/prerequisites.mdx +++ b/docs/get-started/prerequisites.mdx @@ -96,7 +96,7 @@ The table comes from [`ci/platform-matrix.json`](https://github.com/NVIDIA/NemoC | DGX OS (Spark) | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | Tested with limitations across qualified profiles on one physical DGX Station GB300; see [Additional Setup for DGX Station](additional-setup/dgx-station-preparation) for accepted profiles, the pending no-OTA DGX OS `7.6.x` end-to-end qualification, runtime gates, and current dual-Station and dedicated CI limitations. | | Linux | Docker | Tested | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and no openshell launchd service is loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and launchctl reports the openshell launchd service is not loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | Requires WSL2 with Docker Desktop backend. See [Additional Setup for Windows Machines](additional-setup/windows-preparation) before the Quickstart. | For the complete platform support matrix, including all deferred platforms and CI coverage, refer to [Platform Support](../reference/platform-support). diff --git a/docs/reference/architecture.mdx b/docs/reference/architecture.mdx index caab09a7b99..4ed23824b97 100644 --- a/docs/reference/architecture.mdx +++ b/docs/reference/architecture.mdx @@ -91,8 +91,9 @@ On Apple Silicon macOS, Homebrew makes the official OpenShell formula authoritat The installer stages the formula and onboarding starts its `openshell` service. When Homebrew is present, a missing formula, a formula from another tap, a service-start failure, or a health failure stops onboarding. Homebrew 6.x refuses to load formulae from taps it has not marked trusted, so it can fail to confirm the official formula's identity. -When that happens and no `openshell` launchd service is loaded, onboarding warns and uses the standalone macOS gateway fallback. +When that happens, onboarding falls back to the standalone macOS gateway only after `launchctl` reports the `homebrew.mxcl.openshell` service is not loaded. A loaded launchd service keeps its lifecycle authority, so onboarding stops until the service is stopped. +An unanswered `launchctl` probe also stops onboarding, because it does not establish that the service is stopped. A host without Homebrew also uses the standalone macOS gateway fallback. NemoClaw-managed gateways on custom ports remain detached and separate from the default service. An externally supervised gateway can use any matching configured port; its declared supervisor retains lifecycle authority. diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index 79d2a948f13..5ee70e1dbc9 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -81,7 +81,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Spark) | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and no openshell launchd service is loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and launchctl reports the openshell launchd service is not loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. | {/* platform-matrix-full:end */} diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 094eba7c731..4ba9c1f3fff 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -913,6 +913,7 @@ Follow these steps to reconnect. If Homebrew is present but `openshell` is missing or comes from another tap, rerun the NemoClaw installer. If Homebrew refuses to load the formula and cannot confirm its identity, onboarding warns with Homebrew's reason and continues on the standalone fallback. If the `homebrew.mxcl.openshell` launchd service is still loaded when that happens, onboarding stops instead and asks you to stop the service first, because NemoClaw cannot manage or safely replace a service launchd owns. + Onboarding also stops when `launchctl` cannot run, because an unanswered probe does not show the service is stopped. On Linux package installs, inspect and restart the upstream service. diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index 61e557e78d0..b279994a4a9 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -183,7 +183,28 @@ describe("docker-driver-gateway-service", () => { : spawnResult(), ), }), - ).toThrow("launchd service homebrew.mxcl.openshell is loaded"); + ).toThrow("its launchd service homebrew.mxcl.openshell is loaded"); + }); + + it.each([ + ["cannot run", { error: new Error("spawn launchctl ENOENT"), status: null }], + ["reports no exit status", { status: null }], + ])("aborts instead of falling back when the launchd probe %s (#7707)", (_case, launchdResult) => { + const refusal = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + expect(() => + hasOpenShellGatewayUserService({ + commandExists: () => true, + platform: "darwin", + spawnSyncImpl: vi.fn((command: string, args: string[]) => + command === "launchctl" + ? launchdResult + : args[0] === "info" + ? spawnResult(1, refusal) + : spawnResult(), + ), + }), + ).toThrow("could not determine whether its launchd service homebrew.mxcl.openshell is loaded"); }); it.each([ @@ -196,6 +217,14 @@ describe("docker-driver-gateway-service", () => { "a refusal naming a tap that only starts with the pinned name", "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell-fork.", ], + [ + "a refusal naming a dot-separated neighbor of the pinned tap", + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.fork.", + ], + [ + "a refusal naming another formula from the pinned tap", + "Error: Refusing to load formula nvidia/openshell/openshell-extra from untrusted tap nvidia/openshell.", + ], ])("keeps %s fatal during the formula identity check (#7707)", (_case, reason) => { expect(() => hasOpenShellGatewayUserService({ diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index fe019497e60..abf6d7637e4 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -250,26 +250,41 @@ const warnedHomebrewIdentityCheckReasons = new Set(); // it, this case reverts to the fail-closed abort, not to a bypass. Remove this // branch when supported Homebrew versions can verify the pinned formula // identity again. -const PINNED_TAP_LOAD_REFUSAL_PATTERN = new RegExp( - `Refusing to load formula ${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} from untrusted tap ${OPENSHELL_GATEWAY_HOMEBREW_TAP}(?![\\w/-])`, -); +// Compare the named formula and tap exactly rather than matching a prefix, so +// a refusal naming a neighboring tap such as nvidia/openshell-fork or +// nvidia/openshell.fork stays fatal. The tap ends the sentence, so one +// trailing period is part of the message rather than the name. +const HOMEBREW_LOAD_REFUSAL_PATTERN = /Refusing to load formula (\S+) from untrusted tap (\S+)/; function isPinnedTapLoadRefusal(reason: string): boolean { - return PINNED_TAP_LOAD_REFUSAL_PATTERN.test(reason.replace(/\s+/g, " ")); + const match = HOMEBREW_LOAD_REFUSAL_PATTERN.exec(reason.replace(/\s+/g, " ")); + return ( + match !== null && + match[1] === `${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}` && + match[2].replace(/\.$/, "") === OPENSHELL_GATEWAY_HOMEBREW_TAP + ); } // A loaded launchd unit means launchd still owns the service lifecycle even // when Homebrew refuses to load the formula: the standalone cutover path could // adopt that process or kill one launchd would restart. launchctl answers -// without loading the formula, so probe it before degrading. -function isHomebrewGatewayLaunchdUnitLoaded( +// without loading the formula, so probe it before degrading. Only a launchctl +// run that completed and reported the unit missing proves it is safe to fall +// back; a probe that could not run proves nothing and must not degrade. +function readHomebrewGatewayLaunchdUnitState( opts: Required>, -): boolean { - return runCommand( +): "loaded" | "not-loaded" | "unknown" { + const result = opts.spawnSyncImpl( "launchctl", ["list", `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`], - opts, - ).ok; + { + encoding: "utf-8", + env: opts.env, + stdio: ["ignore", "pipe", "pipe"], + } satisfies SpawnSyncOptions, + ); + if (result.error || typeof result.status !== "number") return "unknown"; + return result.status === 0 ? "loaded" : "not-loaded"; } function warnHomebrewIdentityCheckUnavailable(reason: string): void { @@ -305,10 +320,17 @@ function hasOfficialHomebrewFormula( if (!isPinnedTapLoadRefusal(reason)) { throw new Error(`OpenShell Homebrew formula identity check failed: ${reason}`); } - if (isHomebrewGatewayLaunchdUnitLoaded({ env, spawnSyncImpl })) { + const launchdState = readHomebrewGatewayLaunchdUnitState({ env, spawnSyncImpl }); + if (launchdState !== "not-loaded") { + const unit = `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`; + const situation = + launchdState === "loaded" + ? `its launchd service ${unit} is loaded` + : `NemoClaw could not determine whether its launchd service ${unit} is loaded`; throw new Error( - `Homebrew refused to load the pinned OpenShell formula while its launchd service homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} is loaded. ` + - `NemoClaw cannot manage or safely replace that service. Stop it (launchctl bootout gui//homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}) and rerun onboarding. ` + + `Homebrew refused to load the pinned OpenShell formula and ${situation}. ` + + `NemoClaw cannot manage or safely replace a service launchd owns. ` + + `Stop it (launchctl bootout gui//${unit}) and rerun onboarding. ` + `Homebrew reported: ${reason}`, ); } From 48d2974187cfb09c239c766c444547159edb7b15 Mon Sep 17 00:00:00 2001 From: harjoth Date: Tue, 28 Jul 2026 10:10:43 -0700 Subject: [PATCH 07/12] fix(onboard): treat an unrecognized launchctl failure as an unknown unit state Review found the probe still read every completed nonzero exit as proof the unit was absent. launchctl reports a missing service as exit 113 with "Could not find service"; only those signals now mean not-loaded, and any other failure stays unknown and keeps the abort. Refs: #7707 Signed-off-by: harjoth --- docs/reference/troubleshooting.mdx | 2 +- .../docker-driver-gateway-service.test.ts | 24 ++++++++++++++++++- .../onboard/docker-driver-gateway-service.ts | 15 ++++++++++-- ...ost-runtime-homebrew-untrusted-tap.test.ts | 6 ++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 4ba9c1f3fff..1aa2b2e7bc2 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -913,7 +913,7 @@ Follow these steps to reconnect. If Homebrew is present but `openshell` is missing or comes from another tap, rerun the NemoClaw installer. If Homebrew refuses to load the formula and cannot confirm its identity, onboarding warns with Homebrew's reason and continues on the standalone fallback. If the `homebrew.mxcl.openshell` launchd service is still loaded when that happens, onboarding stops instead and asks you to stop the service first, because NemoClaw cannot manage or safely replace a service launchd owns. - Onboarding also stops when `launchctl` cannot run, because an unanswered probe does not show the service is stopped. + Onboarding also stops when `launchctl` cannot run or fails for a reason other than a missing service, because a probe that does not report the service missing has not shown it is stopped. On Linux package installs, inspect and restart the upstream service. diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index b279994a4a9..dfe1a3e648a 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -155,7 +155,7 @@ describe("docker-driver-gateway-service", () => { platform: "darwin" as NodeJS.Platform, spawnSyncImpl: vi.fn((command: string, args: string[]) => command === "launchctl" - ? spawnResult(1, "Could not find service") + ? spawnResult(113, 'Could not find service "homebrew.mxcl.openshell" in domain for port') : args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), @@ -186,9 +186,31 @@ describe("docker-driver-gateway-service", () => { ).toThrow("its launchd service homebrew.mxcl.openshell is loaded"); }); + it("falls back when launchctl names the missing unit without the missing-unit status (#7707)", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + const refusal = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + + expect( + hasOpenShellGatewayUserService({ + commandExists: () => true, + platform: "darwin", + spawnSyncImpl: vi.fn((command: string, args: string[]) => + command === "launchctl" + ? spawnResult(1, 'Could not find service "homebrew.mxcl.openshell"') + : args[0] === "info" + ? spawnResult(1, refusal) + : spawnResult(), + ), + }), + ).toBe(false); + }); + it.each([ ["cannot run", { error: new Error("spawn launchctl ENOENT"), status: null }], ["reports no exit status", { status: null }], + ["fails for an unrecognized reason", spawnResult(1, "Bootstrap failed: 5: Input/output error")], + ["is denied", spawnResult(1, "Operation not permitted")], ])("aborts instead of falling back when the launchd probe %s (#7707)", (_case, launchdResult) => { const refusal = "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index abf6d7637e4..4d4f7621d5d 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -265,12 +265,19 @@ function isPinnedTapLoadRefusal(reason: string): boolean { ); } +// launchctl reports a missing service as exit 113 with "Could not find +// service". Either signal alone establishes the unit is absent; a failure that +// carries neither says nothing about the unit, so it stays unknown. +const LAUNCHCTL_SERVICE_MISSING_STATUS = 113; +const LAUNCHCTL_SERVICE_MISSING_PATTERN = /Could not find service/; + // A loaded launchd unit means launchd still owns the service lifecycle even // when Homebrew refuses to load the formula: the standalone cutover path could // adopt that process or kill one launchd would restart. launchctl answers // without loading the formula, so probe it before degrading. Only a launchctl // run that completed and reported the unit missing proves it is safe to fall -// back; a probe that could not run proves nothing and must not degrade. +// back; a probe that could not run, or that failed for any other reason, +// proves nothing and must not degrade. function readHomebrewGatewayLaunchdUnitState( opts: Required>, ): "loaded" | "not-loaded" | "unknown" { @@ -284,7 +291,11 @@ function readHomebrewGatewayLaunchdUnitState( } satisfies SpawnSyncOptions, ); if (result.error || typeof result.status !== "number") return "unknown"; - return result.status === 0 ? "loaded" : "not-loaded"; + if (result.status === 0) return "loaded"; + return result.status === LAUNCHCTL_SERVICE_MISSING_STATUS || + LAUNCHCTL_SERVICE_MISSING_PATTERN.test(text(result.stderr)) + ? "not-loaded" + : "unknown"; } function warnHomebrewIdentityCheckUnavailable(reason: string): void { diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts index 3faed79087f..4f3cc3c2346 100644 --- a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -23,7 +23,11 @@ vi.mock("node:child_process", async (importOriginal) => { stdout: "", } : command === "launchctl" - ? { status: 1, stderr: "Could not find service", stdout: "" } + ? { + status: 113, + stderr: 'Could not find service "homebrew.mxcl.openshell" in domain for port', + stdout: "", + } : { status: 0, stderr: "", stdout: "" }, ), }; From 9fe39c1e1f0e2c481be659178bae456c1b18ade4 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Tue, 28 Jul 2026 10:26:48 -0700 Subject: [PATCH 08/12] fix(onboard): recognize only missing launchd unit Signed-off-by: Apurv Kumaria --- .../docker-driver-gateway-service.test.ts | 27 +++++++++++- .../onboard/docker-driver-gateway-service.ts | 41 +++++++++++++------ ...ost-runtime-homebrew-untrusted-tap.test.ts | 8 +++- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index b279994a4a9..5446a7d8bf7 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -36,6 +36,12 @@ function spawnResult(status = 0, stderr = "", stdout = ""): SpawnSyncLikeResult return { status, stderr, stdout }; } +const TEST_UID = 501; +const LAUNCHCTL_MISSING_OPENSHELL_SERVICE = [ + "Bad request.", + `Could not find service "homebrew.mxcl.openshell" in domain for user gui: ${TEST_UID}`, +].join("\n"); + function trustedShowOutput( fragmentPath = "/lib/systemd/user/openshell-gateway.service", execPath = "/usr/bin/openshell-gateway", @@ -152,10 +158,11 @@ describe("docker-driver-gateway-service", () => { "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; const options = { commandExists: () => true, + getuid: () => TEST_UID, platform: "darwin" as NodeJS.Platform, spawnSyncImpl: vi.fn((command: string, args: string[]) => command === "launchctl" - ? spawnResult(1, "Could not find service") + ? spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE) : args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), @@ -164,6 +171,11 @@ describe("docker-driver-gateway-service", () => { expect(hasOpenShellGatewayUserService(options)).toBe(false); expect(hasOpenShellGatewayUserService(options)).toBe(false); + expect(options.spawnSyncImpl).toHaveBeenCalledWith( + "launchctl", + ["print", `gui/${TEST_UID}/homebrew.mxcl.openshell`], + expect.any(Object), + ); expect(warn).toHaveBeenCalledTimes(1); expect(warn).toHaveBeenCalledWith(expect.stringContaining(refusal)); }); @@ -174,6 +186,7 @@ describe("docker-driver-gateway-service", () => { expect(() => hasOpenShellGatewayUserService({ commandExists: () => true, + getuid: () => TEST_UID, platform: "darwin", spawnSyncImpl: vi.fn((command: string, args: string[]) => command === "launchctl" @@ -187,6 +200,14 @@ describe("docker-driver-gateway-service", () => { }); it.each([ + ["returns a generic nonzero result", spawnResult(1, "Operation not permitted")], + [ + "reports a different missing unit", + spawnResult( + 113, + 'Bad request.\nCould not find service "homebrew.mxcl.other" in domain for user gui: 501', + ), + ], ["cannot run", { error: new Error("spawn launchctl ENOENT"), status: null }], ["reports no exit status", { status: null }], ])("aborts instead of falling back when the launchd probe %s (#7707)", (_case, launchdResult) => { @@ -195,6 +216,7 @@ describe("docker-driver-gateway-service", () => { expect(() => hasOpenShellGatewayUserService({ commandExists: () => true, + getuid: () => TEST_UID, platform: "darwin", spawnSyncImpl: vi.fn((command: string, args: string[]) => command === "launchctl" @@ -262,10 +284,11 @@ describe("docker-driver-gateway-service", () => { hasOpenShellGatewayUserService: () => hasOpenShellGatewayUserService({ commandExists: () => true, + getuid: () => TEST_UID, platform: "darwin", spawnSyncImpl: (command: string, args: string[]) => command === "launchctl" - ? spawnResult(1, "Could not find service") + ? spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE) : args[0] === "info" ? spawnResult(1, refusal) : spawnResult(), diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index abf6d7637e4..3ed98635989 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -27,6 +27,7 @@ export interface OpenShellGatewayUserServiceOptions { commandExists?: (command: string) => boolean; env?: NodeJS.ProcessEnv; existsSync?: (filePath: string) => boolean; + getuid?: () => number; home?: string; lstatSync?: typeof fs.lstatSync; platform?: NodeJS.Platform; @@ -272,19 +273,29 @@ function isPinnedTapLoadRefusal(reason: string): boolean { // run that completed and reported the unit missing proves it is safe to fall // back; a probe that could not run proves nothing and must not degrade. function readHomebrewGatewayLaunchdUnitState( - opts: Required>, + opts: Required> & + Pick, ): "loaded" | "not-loaded" | "unknown" { - const result = opts.spawnSyncImpl( - "launchctl", - ["list", `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`], - { - encoding: "utf-8", - env: opts.env, - stdio: ["ignore", "pipe", "pipe"], - } satisfies SpawnSyncOptions, - ); + const getuid = opts.getuid ?? process.getuid; + const uid = getuid?.(); + if (!Number.isSafeInteger(uid) || Number(uid) < 0) return "unknown"; + const unit = `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`; + const result = opts.spawnSyncImpl("launchctl", ["print", `gui/${String(uid)}/${unit}`], { + encoding: "utf-8", + env: opts.env, + stdio: ["ignore", "pipe", "pipe"], + } satisfies SpawnSyncOptions); if (result.error || typeof result.status !== "number") return "unknown"; - return result.status === 0 ? "loaded" : "not-loaded"; + if (result.status === 0) return "loaded"; + const missingUnitError = [ + "Bad request.", + `Could not find service "${unit}" in domain for user gui: ${String(uid)}`, + ].join("\n"); + return result.status === 113 && + text(result.stdout).trim() === "" && + text(result.stderr).trim() === missingUnitError + ? "not-loaded" + : "unknown"; } function warnHomebrewIdentityCheckUnavailable(reason: string): void { @@ -298,7 +309,7 @@ function warnHomebrewIdentityCheckUnavailable(reason: string): void { function hasOfficialHomebrewFormula( opts: Pick< OpenShellGatewayUserServiceOptions, - "commandExists" | "env" | "platform" | "spawnSyncImpl" + "commandExists" | "env" | "getuid" | "platform" | "spawnSyncImpl" >, ): boolean { if ((opts.platform ?? process.platform) !== "darwin") return false; @@ -320,7 +331,11 @@ function hasOfficialHomebrewFormula( if (!isPinnedTapLoadRefusal(reason)) { throw new Error(`OpenShell Homebrew formula identity check failed: ${reason}`); } - const launchdState = readHomebrewGatewayLaunchdUnitState({ env, spawnSyncImpl }); + const launchdState = readHomebrewGatewayLaunchdUnitState({ + env, + getuid: opts.getuid, + spawnSyncImpl, + }); if (launchdState !== "not-loaded") { const unit = `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`; const situation = diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts index 3faed79087f..00fb8f44d25 100644 --- a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -23,7 +23,12 @@ vi.mock("node:child_process", async (importOriginal) => { stdout: "", } : command === "launchctl" - ? { status: 1, stderr: "Could not find service", stdout: "" } + ? { + status: 113, + stderr: + 'Bad request.\nCould not find service "homebrew.mxcl.openshell" in domain for user gui: 501', + stdout: "", + } : { status: 0, stderr: "", stdout: "" }, ), }; @@ -33,6 +38,7 @@ const ORIGINAL_ENV = { ...process.env }; beforeEach(() => { vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + vi.spyOn(process, "getuid").mockReturnValue(501); vi.spyOn(console, "warn").mockImplementation(() => {}); }); From 3f8825bd98ac5f2ea4edda8843b86b84f23f3267 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sun, 2 Aug 2026 06:53:33 -0700 Subject: [PATCH 09/12] fix(onboard): harden Homebrew fallback diagnostics Signed-off-by: Carlos Villela --- ci/platform-matrix.json | 2 +- docs/get-started/prerequisites.mdx | 2 +- docs/reference/architecture.mdx | 11 +- docs/reference/platform-support.mdx | 2 +- docs/reference/troubleshooting.mdx | 8 +- .../docker-driver-gateway-service.test.ts | 252 ++++++++++++++---- .../onboard/docker-driver-gateway-service.ts | 98 +++---- ...ost-runtime-homebrew-untrusted-tap.test.ts | 127 +++++++-- 8 files changed, 377 insertions(+), 125 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 7f7928c9ce3..58a0f02cf52 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -39,7 +39,7 @@ "status": "caveated", "prd_priority": "P0", "ci_tested": true, - "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and launchctl reports the openshell launchd service is not loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight." + "notes": "Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew can load the pinned official OpenShell formula, the gateway appears in `brew services list` as `openshell`. When Homebrew 6.x returns the exact pinned-formula untrusted-tap refusal from `brew info`, NemoClaw checks the matching launchd unit with `launchctl print`. NemoClaw uses the detached standalone gateway fallback only when that command returns the exact missing-service result for `homebrew.mxcl.openshell`. Without Homebrew, NemoClaw uses the standalone OpenShell install and the same fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends but does not require them during preflight." }, { "name": "DGX OS (Spark)", diff --git a/docs/get-started/prerequisites.mdx b/docs/get-started/prerequisites.mdx index 6895ab5382e..bb3c263aac3 100644 --- a/docs/get-started/prerequisites.mdx +++ b/docs/get-started/prerequisites.mdx @@ -104,7 +104,7 @@ The table comes from [`ci/platform-matrix.json`](https://github.com/NVIDIA/NemoC | DGX OS (Spark) | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | Tested with limitations across qualified profiles on one physical DGX Station GB300; see [Additional Setup for DGX Station](additional-setup/dgx-station-preparation) for accepted profiles, the pending no-OTA DGX OS `7.6.x` end-to-end qualification, runtime gates, and current dual-Station and dedicated CI limitations. | | Linux | Docker | Tested | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and launchctl reports the openshell launchd service is not loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew can load the pinned official OpenShell formula, the gateway appears in `brew services list` as `openshell`. When Homebrew 6.x returns the exact pinned-formula untrusted-tap refusal from `brew info`, NemoClaw checks the matching launchd unit with `launchctl print`. NemoClaw uses the detached standalone gateway fallback only when that command returns the exact missing-service result for `homebrew.mxcl.openshell`. Without Homebrew, NemoClaw uses the standalone OpenShell install and the same fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends but does not require them during preflight. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | Requires WSL2 with Docker Desktop backend. See [Additional Setup for Windows Machines](additional-setup/windows-preparation) before the Quickstart. | For the complete platform support matrix, including all deferred platforms and CI coverage, refer to [Platform Support](../reference/platform-support). diff --git a/docs/reference/architecture.mdx b/docs/reference/architecture.mdx index ee8b6025ed2..392ace097aa 100644 --- a/docs/reference/architecture.mdx +++ b/docs/reference/architecture.mdx @@ -92,11 +92,14 @@ The standalone Linux process is used only when the systemd user manager is unava On Apple Silicon macOS, Homebrew makes the official OpenShell formula authoritative. The installer stages the formula and onboarding starts its `openshell` service. When Homebrew is present, a missing formula, a formula from another tap, a service-start failure, or a health failure stops onboarding. -Homebrew 6.x refuses to load formulae from taps it has not marked trusted, so it can fail to confirm the official formula's identity. -When that happens, onboarding falls back to the standalone macOS gateway only after `launchctl` reports the `homebrew.mxcl.openshell` service is not loaded. -A loaded launchd service keeps its lifecycle authority, so onboarding stops until the service is stopped. -An unanswered `launchctl` probe also stops onboarding, because it does not establish that the service is stopped. +Homebrew 6.x can return a pinned-formula untrusted-tap refusal from `brew info --json=v2 openshell` after `brew list --formula openshell` reports the formula installed. +Only the complete refusal for `nvidia/openshell/openshell` from `nvidia/openshell` can select the standalone macOS gateway fallback. +Before the fallback, `launchctl print` must return the exact missing-service result for `homebrew.mxcl.openshell`. +Onboarding prints the recognized diagnostic one time without repeating raw Homebrew output when both conditions match. +Every other Homebrew or `launchctl` error stops onboarding. +A loaded `homebrew.mxcl.openshell` service remains under launchd lifecycle authority, so onboarding does not stop, replace, or adopt its process. A host without Homebrew also uses the standalone macOS gateway fallback. + NemoClaw-managed gateways on custom ports remain detached and separate from the default service. An externally supervised gateway can use any matching configured port; its declared supervisor retains lifecycle authority. In both Docker-driver modes, the sandbox is a Docker container, not a Kubernetes pod. diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index 9e5097e3feb..a632fad5d7e 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -81,7 +81,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Spark) | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). | | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | -| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew is available, OpenShell uses its official formula and the gateway appears in `brew services list` as `openshell`; without Homebrew, or when Homebrew refuses to load the official formula from its pinned tap and launchctl reports the openshell launchd service is not loaded, NemoClaw uses the standalone OpenShell install and detached gateway fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. | +| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. When Homebrew can load the pinned official OpenShell formula, the gateway appears in `brew services list` as `openshell`. When Homebrew 6.x returns the exact pinned-formula untrusted-tap refusal from `brew info`, NemoClaw checks the matching launchd unit with `launchctl print`. NemoClaw uses the detached standalone gateway fallback only when that command returns the exact missing-service result for `homebrew.mxcl.openshell`. Without Homebrew, NemoClaw uses the standalone OpenShell install and the same fallback. Homebrew Colima users must install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends but does not require them during preflight. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. | {/* platform-matrix-full:end */} diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index c3a4c863164..4ac3a07f143 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -932,9 +932,11 @@ Follow these steps to reconnect. ``` If Homebrew is present but `openshell` is missing or comes from another tap, rerun the NemoClaw installer. - If Homebrew refuses to load the formula and cannot confirm its identity, onboarding warns with Homebrew's reason and continues on the standalone fallback. - If the `homebrew.mxcl.openshell` launchd service is still loaded when that happens, onboarding stops instead and asks you to stop the service first, because NemoClaw cannot manage or safely replace a service launchd owns. - Onboarding also stops when `launchctl` cannot run or fails for a reason other than a missing service, because a probe that does not report the service missing has not shown it is stopped. + If `brew info --json=v2 openshell` returns the exact pinned-formula untrusted-tap refusal, onboarding prints the recognized diagnostic once without repeating raw Homebrew output. + It selects the standalone fallback only when `launchctl print` also returns the exact missing-service result for `homebrew.mxcl.openshell`. + Every other Homebrew or `launchctl` error stops onboarding. + A loaded `homebrew.mxcl.openshell` service remains under launchd lifecycle authority. + NemoClaw does not stop, replace, or adopt that process, and the error provides the `launchctl bootout` remediation command. On Linux package installs, inspect and restart the upstream service. diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index 0ea7e6c9e1a..2ae7da26834 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -12,6 +12,7 @@ import { getTrustedActiveOpenShellGatewayUserServicePid, hasOpenShellGatewayUserService, NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE_MARKER, + type OpenShellGatewayUserServiceOptions, type SpawnSyncLikeResult, startOpenShellGatewayUserService, startPackageManagedDockerDriverGateway, @@ -42,6 +43,56 @@ const LAUNCHCTL_MISSING_OPENSHELL_SERVICE = [ "Bad request.", `Could not find service "homebrew.mxcl.openshell" in domain for user gui: ${TEST_UID}`, ].join("\n"); +const HOMEBREW_PINNED_TAP_LOAD_REFUSAL = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + +function expectHomebrewGateFailureBeforeMutation( + brewInfoResult: SpawnSyncLikeResult, + launchctlResult: SpawnSyncLikeResult, + expectedError: string, +): void { + const operations: Array<(options: OpenShellGatewayUserServiceOptions) => unknown> = [ + hasOpenShellGatewayUserService, + startOpenShellGatewayUserService, + stopOpenShellGatewayUserService, + ]; + + for (const operation of operations) { + const preparePortForServiceStart = vi.fn(); + const prepareServiceEnv = vi.fn(); + const validatePortOwnerForServiceStart = vi.fn(); + const spawnSyncImpl = vi.fn((command: string, args: string[]) => + command === "launchctl" + ? launchctlResult + : args[0] === "info" + ? brewInfoResult + : spawnResult(), + ); + + expect(() => + operation({ + commandExists: () => true, + getuid: () => TEST_UID, + platform: "darwin", + preparePortForServiceStart, + prepareServiceEnv, + spawnSyncImpl, + validatePortOwnerForServiceStart, + }), + ).toThrow(expectedError); + expect(preparePortForServiceStart).not.toHaveBeenCalled(); + expect(prepareServiceEnv).not.toHaveBeenCalled(); + expect(validatePortOwnerForServiceStart).not.toHaveBeenCalled(); + expect( + spawnSyncImpl.mock.calls.some( + ([command, args]) => + (command === "brew" && args[0] === "services") || + (command === "launchctl" && args[0] !== "print") || + command === "kill", + ), + ).toBe(false); + } +} function trustedShowOutput( fragmentPath = "/lib/systemd/user/openshell-gateway.service", @@ -153,10 +204,8 @@ describe("docker-driver-gateway-service", () => { ).toThrow("must come from nvidia/openshell"); }); - it("continues without the Homebrew service when brew refuses to load the formula (#7707)", () => { + it("continues without the Homebrew service only for the exact pinned-tap refusal (#7707)", () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - const refusal = - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; const options = { commandExists: () => true, getuid: () => TEST_UID, @@ -165,7 +214,7 @@ describe("docker-driver-gateway-service", () => { command === "launchctl" ? spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE) : args[0] === "info" - ? spawnResult(1, refusal) + ? spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL) : spawnResult(), ), }; @@ -178,30 +227,84 @@ describe("docker-driver-gateway-service", () => { expect.any(Object), ); expect(warn).toHaveBeenCalledTimes(1); - expect(warn).toHaveBeenCalledWith(expect.stringContaining(refusal)); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("using the standalone gateway fallback"), + ); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("Refusing to load formula")); }); it("aborts instead of falling back while the Homebrew launchd service is loaded (#7707)", () => { - const refusal = - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + const spawn = vi.fn((command: string, args: string[]) => + command === "launchctl" + ? spawnResult(0, "", '{"PID" = 4242;}') + : args[0] === "info" + ? spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL) + : spawnResult(), + ); + expect(() => hasOpenShellGatewayUserService({ commandExists: () => true, getuid: () => TEST_UID, platform: "darwin", - spawnSyncImpl: vi.fn((command: string, args: string[]) => - command === "launchctl" - ? spawnResult(0, "", '{"PID" = 4242;}') - : args[0] === "info" - ? spawnResult(1, refusal) - : spawnResult(), - ), + spawnSyncImpl: spawn, }), ).toThrow("its launchd service homebrew.mxcl.openshell is loaded"); + expect(spawn.mock.calls.map(([command, args]) => [command, ...args])).toEqual([ + ["brew", "list", "--formula", "openshell"], + ["brew", "info", "--json=v2", "openshell"], + ["launchctl", "print", `gui/${TEST_UID}/homebrew.mxcl.openshell`], + ]); }); it.each([ ["returns a generic nonzero result", spawnResult(1, "Operation not permitted")], + [ + "returns the exact missing-service text with a different status", + spawnResult(1, LAUNCHCTL_MISSING_OPENSHELL_SERVICE), + ], + [ + "adds text to the missing-service result", + spawnResult(113, `${LAUNCHCTL_MISSING_OPENSHELL_SERVICE}\nTry again later.`), + ], + [ + "prefixes the missing-service result with whitespace", + spawnResult(113, ` ${LAUNCHCTL_MISSING_OPENSHELL_SERVICE}`), + ], + [ + "suffixes the missing-service result with whitespace", + spawnResult(113, `${LAUNCHCTL_MISSING_OPENSHELL_SERVICE}\n`), + ], + [ + "repeats whitespace in the missing-service result", + spawnResult( + 113, + LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Bad request.", "Bad request."), + ), + ], + [ + "adds a tab to the missing-service result", + spawnResult( + 113, + LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Could not find", "Could not\tfind"), + ), + ], + [ + "inserts a line break in the missing-service result", + spawnResult( + 113, + LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Could not find", "Could not\nfind"), + ), + ], + [ + "uses CRLF in the missing-service result", + spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("\n", "\r\n")), + ], + ["writes whitespace to stdout", spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE, " \n")], + [ + "writes the missing-service result to stdout", + spawnResult(113, "", LAUNCHCTL_MISSING_OPENSHELL_SERVICE), + ], [ "reports a different missing unit", spawnResult( @@ -209,72 +312,131 @@ describe("docker-driver-gateway-service", () => { 'Bad request.\nCould not find service "homebrew.mxcl.other" in domain for user gui: 501', ), ], + [ + "reports the expected unit for a different user", + spawnResult( + 113, + 'Bad request.\nCould not find service "homebrew.mxcl.openshell" in domain for user gui: 502', + ), + ], ["cannot run", { error: new Error("spawn launchctl ENOENT"), status: null }], ["reports no exit status", { status: null }], ])("aborts instead of falling back when the launchd probe %s (#7707)", (_case, launchdResult) => { - const refusal = - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; - expect(() => - hasOpenShellGatewayUserService({ - commandExists: () => true, - getuid: () => TEST_UID, - platform: "darwin", - spawnSyncImpl: vi.fn((command: string, args: string[]) => - command === "launchctl" - ? launchdResult - : args[0] === "info" - ? spawnResult(1, refusal) - : spawnResult(), - ), - }), - ).toThrow("could not determine whether its launchd service homebrew.mxcl.openshell is loaded"); + expectHomebrewGateFailureBeforeMutation( + spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL), + launchdResult, + "could not determine whether its launchd service homebrew.mxcl.openshell is loaded", + ); }); it.each([ - ["a generic brew info failure", "Error: Permission denied"], + ["a generic brew info failure", spawnResult(1, "Error: Permission denied")], [ "a refused foreign-tap formula", - "Error: Refusing to load formula other/tap/openshell from untrusted tap other/tap.", + spawnResult( + 1, + "Error: Refusing to load formula other/tap/openshell from untrusted tap other/tap.", + ), ], [ "a refusal naming a tap that only starts with the pinned name", - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell-fork.", + spawnResult( + 1, + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell-fork.", + ), ], [ "a refusal naming a dot-separated neighbor of the pinned tap", - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.fork.", + spawnResult( + 1, + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.fork.", + ), ], [ "a refusal naming another formula from the pinned tap", - "Error: Refusing to load formula nvidia/openshell/openshell-extra from untrusted tap nvidia/openshell.", + spawnResult( + 1, + "Error: Refusing to load formula nvidia/openshell/openshell-extra from untrusted tap nvidia/openshell.", + ), + ], + [ + "the pinned refusal followed by another diagnostic", + spawnResult(1, `${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}\nPermission denied.`), + ], + [ + "another diagnostic followed by the pinned refusal", + spawnResult(1, `Permission denied.\n${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}`), + ], + [ + "leading whitespace before the pinned refusal", + spawnResult(1, ` ${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}`), + ], + [ + "trailing whitespace after the pinned refusal", + spawnResult(1, `${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}\n`), + ], + [ + "repeated whitespace in the pinned refusal", + spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load formula")), + ], + [ + "a tab in the pinned refusal", + spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load\tformula")), + ], + [ + "an inserted line break in the pinned refusal", + spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load\nformula")), ], - ])("keeps %s fatal during the formula identity check (#7707)", (_case, reason) => { + [ + "CRLF in the pinned refusal", + spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load\r\nformula")), + ], + [ + "stdout alongside the exact pinned refusal", + spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL, "unexpected stdout"), + ], + [ + "a spawn error alongside the exact pinned refusal", + { + error: new Error("spawn brew failed"), + status: 1, + stderr: HOMEBREW_PINNED_TAP_LOAD_REFUSAL, + stdout: "", + }, + ], + ])("keeps %s fatal during the formula identity check (#7707)", (_case, brewInfoResult) => { + expectHomebrewGateFailureBeforeMutation( + brewInfoResult, + spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE), + "OpenShell Homebrew formula identity check failed; " + + "the unrecognized Homebrew diagnostic was omitted.", + ); + }); + + it("omits unrecognized Homebrew diagnostics from fatal output (#7707)", () => { + const secret = "api_key=opaque-homebrew-diagnostic"; expect(() => hasOpenShellGatewayUserService({ commandExists: () => true, platform: "darwin", spawnSyncImpl: vi.fn((_command: string, args: string[]) => - args[0] === "info" ? spawnResult(1, reason) : spawnResult(), + args[0] === "info" ? spawnResult(1, `Error: Permission denied ${secret}`) : spawnResult(), ), }), - ).toThrow(`OpenShell Homebrew formula identity check failed: ${reason}`); + ).toThrow(expect.not.stringContaining(secret)); }); - it("still reports a missing formula when brew list is blocked by the untrusted-tap refusal (#7707)", () => { - const refusal = - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; + it("still reports a missing formula when brew list returns the pinned-tap refusal (#7707)", () => { expect(() => hasOpenShellGatewayUserService({ commandExists: () => true, platform: "darwin", - spawnSyncImpl: () => spawnResult(1, refusal), + spawnSyncImpl: () => spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL), }), ).toThrow("official OpenShell Homebrew formula is not installed"); }); it("skips the Homebrew-managed start when the formula identity is unconfirmed (#7707)", async () => { - const refusal = - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; const startService = vi.fn(() => { throw new Error("managed start must not run"); }); @@ -291,7 +453,7 @@ describe("docker-driver-gateway-service", () => { command === "launchctl" ? spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE) : args[0] === "info" - ? spawnResult(1, refusal) + ? spawnResult(1, HOMEBREW_PINNED_TAP_LOAD_REFUSAL) : spawnResult(), }), registerDockerDriverGatewayEndpoint: () => true, diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index ec2cbb783e6..5145a373420 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -180,21 +180,34 @@ function runCommand( command: string, args: string[], opts: Required>, -): { ok: boolean; reason?: string; stdout?: string } { +): { + ok: boolean; + rawStderr: string; + rawStdout: string; + reason?: string; + spawnError?: Error; + status: number | null; + stdout?: string; +} { const result = opts.spawnSyncImpl(command, args, { encoding: "utf-8", env: opts.env, stdio: ["ignore", "pipe", "pipe"], } satisfies SpawnSyncOptions); - if (result.error) return { ok: false, reason: result.error.message }; + const rawStderr = text(result.stderr); + const rawStdout = text(result.stdout); + const rawResult = { rawStderr, rawStdout, status: result.status }; + if (result.error) { + return { ...rawResult, ok: false, reason: result.error.message, spawnError: result.error }; + } if (result.status !== 0) { return { + ...rawResult, ok: false, - reason: - text(result.stderr).trim() || text(result.stdout).trim() || `exit ${String(result.status)}`, + reason: rawStderr.trim() || rawStdout.trim() || `exit ${String(result.status)}`, }; } - return { ok: true, stdout: text(result.stdout) }; + return { ...rawResult, ok: true, stdout: rawStdout }; } function runSystemctlUser( @@ -265,44 +278,33 @@ function hasUpstreamOpenShellGatewayUserService( const warnedHomebrewIdentityCheckReasons = new Set(); -// Homebrew 6.x refuses to load formulae from taps it has not marked trusted. -// For the pinned official formula that refusal names the right tap, so it is -// not evidence of a wrong formula, and managing the service through brew would -// fail the same way. Continue on the standalone gateway fallback instead of -// aborting; any other brew failure keeps the fail-closed abort (#7707). -// The match is against Homebrew's literal refusal text: if Homebrew rewords -// it, this case reverts to the fail-closed abort, not to a bypass. Remove this -// branch when supported Homebrew versions can verify the pinned formula -// identity again. -// Compare the named formula and tap exactly rather than matching a prefix, so -// a refusal naming a neighboring tap such as nvidia/openshell-fork or -// nvidia/openshell.fork stays fatal. The tap ends the sentence, so one -// trailing period is part of the message rather than the name. -const HOMEBREW_LOAD_REFUSAL_PATTERN = /Refusing to load formula (\S+) from untrusted tap (\S+)/; - -function isPinnedTapLoadRefusal(reason: string): boolean { - const match = HOMEBREW_LOAD_REFUSAL_PATTERN.exec(reason.replace(/\s+/g, " ")); +const HOMEBREW_PINNED_TAP_LOAD_REFUSAL = + `Error: Refusing to load formula ${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} ` + + `from untrusted tap ${OPENSHELL_GATEWAY_HOMEBREW_TAP}.`; + +// Only the complete Homebrew 6.x refusal for the pinned formula can relax the +// identity check. Changed or additional diagnostic text fails closed (#7707). +function isPinnedTapLoadRefusal(result: ReturnType): boolean { return ( - match !== null && - match[1] === `${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}` && - match[2].replace(/\.$/, "") === OPENSHELL_GATEWAY_HOMEBREW_TAP + !result.ok && + !result.spawnError && + typeof result.status === "number" && + result.status !== 0 && + result.rawStdout === "" && + result.rawStderr === HOMEBREW_PINNED_TAP_LOAD_REFUSAL ); } -// A loaded launchd unit means launchd still owns the service lifecycle even -// when Homebrew refuses to load the formula: the standalone cutover path could -// adopt that process or kill one launchd would restart. launchctl answers -// without loading the formula, so probe it before degrading. Only a launchctl -// run that completed and reported the unit missing proves it is safe to fall -// back; a probe that could not run, or that failed for any other reason, -// proves nothing and must not degrade. +// `launchctl` must prove that the exact managed unit is absent before +// standalone ownership is selected. Every other outcome leaves authority +// unknown and fails closed. function readHomebrewGatewayLaunchdUnitState( opts: Required> & Pick, ): "loaded" | "not-loaded" | "unknown" { const getuid = opts.getuid ?? process.getuid; const uid = getuid?.(); - if (!Number.isSafeInteger(uid) || Number(uid) < 0) return "unknown"; + if (typeof uid !== "number" || !Number.isSafeInteger(uid) || uid < 0) return "unknown"; const unit = `homebrew.mxcl.${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`; const result = opts.spawnSyncImpl("launchctl", ["print", `gui/${String(uid)}/${unit}`], { encoding: "utf-8", @@ -316,17 +318,19 @@ function readHomebrewGatewayLaunchdUnitState( `Could not find service "${unit}" in domain for user gui: ${String(uid)}`, ].join("\n"); return result.status === 113 && - text(result.stdout).trim() === "" && - text(result.stderr).trim() === missingUnitError + text(result.stdout) === "" && + text(result.stderr) === missingUnitError ? "not-loaded" : "unknown"; } -function warnHomebrewIdentityCheckUnavailable(reason: string): void { - if (warnedHomebrewIdentityCheckReasons.has(reason)) return; - warnedHomebrewIdentityCheckReasons.add(reason); +function warnHomebrewIdentityCheckUnavailable(): void { + const warningKey = HOMEBREW_PINNED_TAP_LOAD_REFUSAL; + if (warnedHomebrewIdentityCheckReasons.has(warningKey)) return; + warnedHomebrewIdentityCheckReasons.add(warningKey); console.warn( - ` Homebrew could not confirm the OpenShell formula identity; falling back to the standalone gateway.\n ${reason}`, + " Homebrew could not confirm the OpenShell formula identity; " + + `using the standalone gateway fallback.\n ${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}`, ); } @@ -351,9 +355,11 @@ function hasOfficialHomebrewFormula( spawnSyncImpl, }); if (!info.ok) { - const reason = info.reason ?? "brew info failed"; - if (!isPinnedTapLoadRefusal(reason)) { - throw new Error(`OpenShell Homebrew formula identity check failed: ${reason}`); + if (!isPinnedTapLoadRefusal(info)) { + throw new Error( + "OpenShell Homebrew formula identity check failed; " + + "the unrecognized Homebrew diagnostic was omitted.", + ); } const launchdState = readHomebrewGatewayLaunchdUnitState({ env, @@ -368,12 +374,12 @@ function hasOfficialHomebrewFormula( : `NemoClaw could not determine whether its launchd service ${unit} is loaded`; throw new Error( `Homebrew refused to load the pinned OpenShell formula and ${situation}. ` + - `NemoClaw cannot manage or safely replace a service launchd owns. ` + - `Stop it (launchctl bootout gui//${unit}) and rerun onboarding. ` + - `Homebrew reported: ${reason}`, + `NemoClaw will not manage or replace a service that launchd owns. ` + + `Stop it with launchctl bootout gui/$(id -u)/${unit}, and rerun onboarding. ` + + `Homebrew reported: ${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}`, ); } - warnHomebrewIdentityCheckUnavailable(reason); + warnHomebrewIdentityCheckUnavailable(); return false; } try { diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts index 00fb8f44d25..ad8272266e4 100644 --- a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -6,37 +6,64 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createGatewayHostRuntime, type GatewayHostRuntimeDeps } from "./gateway-host-runtime"; import type { PortProbeResult } from "./preflight"; -// Homebrew 6.x refuses to load formulae from taps it has not marked trusted, -// so `brew info --json=v2 openshell` fails even though the formula is the -// pinned official one. The refusal reaches the gateway-owner resolution -// through the real spawnSync path, not an injected seam (#7707). +const commandState = vi.hoisted(() => ({ + brewInfo: { + status: 1 as number | null, + stderr: + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.", + stdout: "", + }, + calls: [] as string[][], + launchctl: { + status: 113 as number | null, + stderr: + 'Bad request.\nCould not find service "homebrew.mxcl.openshell" in domain for user gui: 501', + stdout: "", + }, +})); + +// Homebrew 6.x can refuse the pinned formula during brew info. +// This mock keeps the production spawnSync boundary in the owner resolution. vi.mock("node:child_process", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - spawnSync: vi.fn((command: string, args: readonly string[]) => - command === "brew" && args[0] === "info" - ? { - status: 1, - stderr: - "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell.", - stdout: "", - } - : command === "launchctl" - ? { - status: 113, - stderr: - 'Bad request.\nCould not find service "homebrew.mxcl.openshell" in domain for user gui: 501', - stdout: "", - } - : { status: 0, stderr: "", stdout: "" }, - ), + spawnSync: vi.fn((command: string, args: readonly string[]) => { + commandState.calls.push([command, ...args]); + if (command === "brew" && args[0] === "info") return { ...commandState.brewInfo }; + if (command === "launchctl") return { ...commandState.launchctl }; + return { status: 0, stderr: "", stdout: "" }; + }), }; }); const ORIGINAL_ENV = { ...process.env }; +const HOMEBREW_PINNED_TAP_LOAD_REFUSAL = + "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; +const LAUNCHCTL_MISSING_OPENSHELL_SERVICE = + 'Bad request.\nCould not find service "homebrew.mxcl.openshell" in domain for user gui: 501'; +const HOMEBREW_IDENTITY_PROBES = [ + ["sh", "-c", 'command -v "$1" >/dev/null 2>&1', "sh", "brew"], + ["brew", "list", "--formula", "openshell"], + ["brew", "info", "--json=v2", "openshell"], +]; +const HOMEBREW_AND_LAUNCHCTL_PROBES = [ + ...HOMEBREW_IDENTITY_PROBES, + ["launchctl", "print", "gui/501/homebrew.mxcl.openshell"], +]; beforeEach(() => { + commandState.calls.length = 0; + Object.assign(commandState.brewInfo, { + status: 1, + stderr: HOMEBREW_PINNED_TAP_LOAD_REFUSAL, + stdout: "", + }); + Object.assign(commandState.launchctl, { + status: 113, + stderr: LAUNCHCTL_MISSING_OPENSHELL_SERVICE, + stdout: "", + }); vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); vi.spyOn(process, "getuid").mockReturnValue(501); vi.spyOn(console, "warn").mockImplementation(() => {}); @@ -63,8 +90,7 @@ function createDeps(): GatewayHostRuntimeDeps { } describe("gateway host runtime on Homebrew 6.x untrusted tap", () => { - it("resolves a standalone owner instead of aborting when brew refuses the pinned tap (#7707)", () => { - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + it("selects standalone ownership after the exact refusal and missing-unit result (#7707)", () => { const runtime = createGatewayHostRuntime(createDeps()); expect(runtime.getGatewayOwner()).toMatchObject({ @@ -74,6 +100,59 @@ describe("gateway host runtime on Homebrew 6.x untrusted tap", () => { source: "standalone", }); expect(runtime.getGatewayOwner()).toMatchObject({ source: "standalone" }); - expect(warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledTimes(1); + }); + + it.each([ + ["leading whitespace", ` ${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}`, ""], + ["trailing whitespace", `${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}\n`, ""], + [ + "repeated whitespace", + HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load formula"), + "", + ], + ["a tab", HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load\tformula"), ""], + [ + "an inserted line break", + HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load\nformula"), + "", + ], + ["CRLF", HOMEBREW_PINNED_TAP_LOAD_REFUSAL.replace("load formula", "load\r\nformula"), ""], + ["unexpected stdout", HOMEBREW_PINNED_TAP_LOAD_REFUSAL, "unexpected stdout"], + ])("rejects Homebrew diagnostic variation %s before owner selection (#7707)", (_case, stderr, stdout) => { + Object.assign(commandState.brewInfo, { stderr, stdout }); + const runtime = createGatewayHostRuntime(createDeps()); + + expect(() => runtime.getGatewayOwner()).toThrow( + "OpenShell Homebrew formula identity check failed; " + + "the unrecognized Homebrew diagnostic was omitted.", + ); + expect(commandState.calls).toEqual(HOMEBREW_IDENTITY_PROBES); + }); + + it.each([ + ["leading whitespace", ` ${LAUNCHCTL_MISSING_OPENSHELL_SERVICE}`, ""], + ["trailing whitespace", `${LAUNCHCTL_MISSING_OPENSHELL_SERVICE}\n`, ""], + [ + "repeated whitespace", + LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Bad request.", "Bad request."), + "", + ], + ["a tab", LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Could not find", "Could not\tfind"), ""], + [ + "an inserted line break", + LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Could not find", "Could not\nfind"), + "", + ], + ["CRLF", LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("\n", "\r\n"), ""], + ["whitespace-only stdout", LAUNCHCTL_MISSING_OPENSHELL_SERVICE, " \n"], + ])("rejects launchctl diagnostic variation %s before owner selection (#7707)", (_case, stderr, stdout) => { + Object.assign(commandState.launchctl, { stderr, stdout }); + const runtime = createGatewayHostRuntime(createDeps()); + + expect(() => runtime.getGatewayOwner()).toThrow( + "could not determine whether its launchd service homebrew.mxcl.openshell is loaded", + ); + expect(commandState.calls).toEqual(HOMEBREW_AND_LAUNCHCTL_PROBES); }); }); From 829b9a3fcab224a410099b88b5e97dc66a9298ce Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 3 Aug 2026 09:45:13 -0700 Subject: [PATCH 10/12] test(onboard): convert complete CRLF fixtures --- src/lib/onboard/docker-driver-gateway-service.test.ts | 2 +- .../onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index 4d9d53eb33b..d344aa6fe6b 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -314,7 +314,7 @@ describe("docker-driver-gateway-service", () => { ], [ "uses CRLF in the missing-service result", - spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("\n", "\r\n")), + spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replaceAll("\n", "\r\n")), ], ["writes whitespace to stdout", spawnResult(113, LAUNCHCTL_MISSING_OPENSHELL_SERVICE, " \n")], [ diff --git a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts index 79179952359..167a53a152c 100644 --- a/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts +++ b/src/lib/onboard/gateway-host-runtime-homebrew-untrusted-tap.test.ts @@ -146,7 +146,7 @@ describe("gateway host runtime on Homebrew 6.x untrusted tap", () => { LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("Could not find", "Could not\nfind"), "", ], - ["CRLF", LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replace("\n", "\r\n"), ""], + ["CRLF", LAUNCHCTL_MISSING_OPENSHELL_SERVICE.replaceAll("\n", "\r\n"), ""], ["whitespace-only stdout", LAUNCHCTL_MISSING_OPENSHELL_SERVICE, " \n"], ])("rejects launchctl diagnostic variation %s before owner selection (#7707)", (_case, stderr, stdout) => { Object.assign(commandState.launchctl, { stderr, stdout }); From 7920f2afcbb42cf40e6e9f89d8dfc6ce6c541a51 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 3 Aug 2026 10:07:32 -0700 Subject: [PATCH 11/12] docs(onboard): record Homebrew fallback retirement --- src/lib/onboard/docker-driver-gateway-service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index 28265519e0b..4757216ef4e 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -324,6 +324,8 @@ const HOMEBREW_PINNED_TAP_LOAD_REFUSAL = // Only the complete Homebrew 6.x refusal for the pinned formula can relax the // identity check. Changed or additional diagnostic text fails closed (#7707). +// Remove this compatibility path when the minimum supported Homebrew version +// can inspect the pinned formula without trusting or loading its tap. function isPinnedTapLoadRefusal(result: ReturnType): boolean { return ( !result.ok && From 5307aa84236f1e74fd64b1c6ea1f511f6db882da Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Mon, 3 Aug 2026 14:02:37 -0700 Subject: [PATCH 12/12] fix(onboard): fail closed on unknown brew list errors Signed-off-by: Apurv Kumaria --- .../docker-driver-gateway-service.test.ts | 27 ++++++++++++++-- .../onboard/docker-driver-gateway-service.ts | 32 +++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/lib/onboard/docker-driver-gateway-service.test.ts b/src/lib/onboard/docker-driver-gateway-service.test.ts index d344aa6fe6b..0ed3d23242b 100644 --- a/src/lib/onboard/docker-driver-gateway-service.test.ts +++ b/src/lib/onboard/docker-driver-gateway-service.test.ts @@ -47,6 +47,7 @@ const LAUNCHCTL_MISSING_OPENSHELL_SERVICE = [ ].join("\n"); const HOMEBREW_PINNED_TAP_LOAD_REFUSAL = "Error: Refusing to load formula nvidia/openshell/openshell from untrusted tap nvidia/openshell."; +const HOMEBREW_OPENSHELL_NOT_INSTALLED = "Error: No such keg: /opt/homebrew/Cellar/openshell"; function expectHomebrewGateFailureBeforeMutation( brewInfoResult: SpawnSyncLikeResult, @@ -241,6 +242,28 @@ describe("docker-driver-gateway-service", () => { expect(warn).toHaveBeenCalledWith(expect.stringContaining("Refusing to load formula")); }); + it("keeps an altered brew list refusal fatal before probing launchd (#7707)", () => { + const alteredRefusal = `${HOMEBREW_PINNED_TAP_LOAD_REFUSAL}\nPermission denied.`; + const spawn = vi.fn((_command: string, args: string[]) => + args[0] === "list" ? spawnResult(1, alteredRefusal) : spawnResult(), + ); + + expect(() => + hasOpenShellGatewayUserService({ + commandExists: () => true, + getuid: () => TEST_UID, + platform: "darwin", + spawnSyncImpl: spawn, + }), + ).toThrow( + "OpenShell Homebrew formula identity check failed; " + + "the unrecognized Homebrew diagnostic was omitted.", + ); + expect(spawn.mock.calls.map(([command, args]) => [command, ...args])).toEqual([ + ["brew", "list", "--formula", "openshell"], + ]); + }); + it("aborts instead of falling back while the Homebrew launchd service is loaded (#7707)", () => { const spawn = vi.fn((command: string, args: string[]) => command === "launchctl" @@ -467,7 +490,7 @@ describe("docker-driver-gateway-service", () => { hasOpenShellGatewayUserService({ commandExists: () => true, platform: "darwin", - spawnSyncImpl: () => spawnResult(1, "formula not installed"), + spawnSyncImpl: () => spawnResult(1, HOMEBREW_OPENSHELL_NOT_INSTALLED), }), ).toBe(false); }); @@ -1024,7 +1047,7 @@ describe("docker-driver-gateway-service", () => { hasOpenShellGatewayUserService({ commandExists: () => true, platform: "darwin", - spawnSyncImpl: () => spawnResult(1, "formula not installed"), + spawnSyncImpl: () => spawnResult(1, HOMEBREW_OPENSHELL_NOT_INSTALLED), }), managedServiceLogCommand: getOpenShellGatewayManagedServiceLogCommand({ platform: "darwin", diff --git a/src/lib/onboard/docker-driver-gateway-service.ts b/src/lib/onboard/docker-driver-gateway-service.ts index 4757216ef4e..bb733f59af1 100644 --- a/src/lib/onboard/docker-driver-gateway-service.ts +++ b/src/lib/onboard/docker-driver-gateway-service.ts @@ -321,6 +321,18 @@ const warnedHomebrewIdentityCheckReasons = new Set(); const HOMEBREW_PINNED_TAP_LOAD_REFUSAL = `Error: Refusing to load formula ${OPENSHELL_GATEWAY_HOMEBREW_TAP}/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE} ` + `from untrusted tap ${OPENSHELL_GATEWAY_HOMEBREW_TAP}.`; +const HOMEBREW_OPENSHELL_NOT_INSTALLED = `Error: No such keg: /opt/homebrew/Cellar/${OPENSHELL_GATEWAY_HOMEBREW_SERVICE}`; + +function isHomebrewFormulaNotInstalled(result: ReturnType): boolean { + return ( + !result.ok && + !result.spawnError && + typeof result.status === "number" && + result.status !== 0 && + result.rawStdout === "" && + result.rawStderr === HOMEBREW_OPENSHELL_NOT_INSTALLED + ); +} // Only the complete Homebrew 6.x refusal for the pinned formula can relax the // identity check. Changed or additional diagnostic text fails closed (#7707). @@ -416,12 +428,20 @@ function hasOfficialHomebrewFormula( spawnSyncImpl, }); if (!listed.ok) { - allowStandaloneForPinnedTapLoadRefusal(listed, { - env, - getuid: opts.getuid, - spawnSyncImpl, - }); - return false; + if (isHomebrewFormulaNotInstalled(listed)) return false; + if ( + allowStandaloneForPinnedTapLoadRefusal(listed, { + env, + getuid: opts.getuid, + spawnSyncImpl, + }) + ) { + return false; + } + throw new OpenShellGatewayServiceTrustError( + "OpenShell Homebrew formula identity check failed; " + + "the unrecognized Homebrew diagnostic was omitted.", + ); } const info = runBrew(["info", "--json=v2", OPENSHELL_GATEWAY_HOMEBREW_SERVICE], { env,