Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ NemoClaw's default Docker-driver topology does not place the sandbox in an embed
Only the default gateway port `8080` uses a NemoClaw-managed service.

On Linux, NemoClaw prefers the upstream package-managed `openshell-gateway.service`.
Before adoption, NemoClaw asks systemd for the unit's effective `ExecStart`.
When systemd returns the service identity, NemoClaw requires trusted package paths for both the unit and its effective binary.
It then checks the effective binary's version against the blueprint range (`min_openshell_version` through `max_openshell_version`).
This check prevents a supported user-local OpenShell install from hiding an out-of-range binary selected by the package unit's absolute `ExecStart`.
NemoClaw declines a known version below the minimum or a known stable version above the maximum.
An above-maximum development build remains eligible when `NEMOCLAW_OPENSHELL_CHANNEL=dev`, and an undetermined version also remains eligible.
When the systemd identity query fails, the package unit also remains eligible for later startup inspection.
NemoClaw declines the package unit when systemd positively identifies an untrusted unit or effective binary.
For a version rejection, NemoClaw reports the version once.
After either rejection, it selects an existing marked NemoClaw user service when available.
Otherwise, onboarding continues with the standalone gateway fallback.
For tarball installs, the NemoClaw installer stages a marked user-level `nemoclaw-openshell-gateway.service` based on the upstream unit.
Onboarding validates and reuses a healthy selected service.
It enables or restarts the service when startup or verified runtime drift requires it, then checks gateway health.
Expand All @@ -94,7 +105,8 @@ The installer stages the formula and onboarding starts its `openshell` service.
If the service fails inspection, startup, or its health check, NemoClaw prints the formula log command and attempts the standalone fallback.
A host without Homebrew, or with no installed OpenShell formula, also uses the standalone macOS gateway.
On both platforms, standalone startup requires exclusive ownership of the gateway port.
Managed-service trust failures and unsafe environment configuration remain hard failures.
A positively untrusted upstream package service is declined as described above.
Trust failures in a marked NemoClaw service or Homebrew formula, and unsafe environment configuration, remain hard failures.

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.
Expand Down
189 changes: 189 additions & 0 deletions src/lib/onboard/docker-driver-gateway-service-version-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

import {
checkUpstreamGatewayVersion,
getNemoclawOpenShellGatewayUserServicePath,
getOpenShellGatewayUserServiceBinaryPaths,
getOpenShellGatewayUserServicePaths,
hasOpenShellGatewayUserService,
NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE_MARKER_LINE,
resetUpstreamGatewayVersionWarning,
} from "./docker-driver-gateway-service";

const PACKAGE_UNIT = "/usr/lib/systemd/user/openshell-gateway.service";
const PACKAGE_BINARY = "/usr/bin/openshell-gateway";
const BOUNDS = { min: "0.0.85", max: "0.0.85" };

function trustedShowOutput(execPath = PACKAGE_BINARY): string {
return [
`FragmentPath=${PACKAGE_UNIT}`,
`ExecStart={ path=${execPath} ; argv[]=${execPath} ; }`,
].join("\n");
}

/** Only the package-managed unit and binary exist on this host. */
function packageOnly(filePath: string): boolean {
return filePath === PACKAGE_UNIT || filePath === PACKAGE_BINARY;
}

function resolveOptions(version: string, overrides: Record<string, unknown> = {}) {
return {
platform: "linux" as const,
existsSync: packageOnly,
getUpstreamGatewayVersion: () => version,
getUpstreamGatewayVersionBounds: () => BOUNDS,
spawnSyncImpl: () => ({ status: 0, stdout: trustedShowOutput() }),
warn: vi.fn(),
...overrides,
};
}

describe("package-managed gateway version gate (#8094)", () => {
beforeEach(() => resetUpstreamGatewayVersionWarning());

it("declines a package gateway newer than the blueprint maximum", () => {
expect(checkUpstreamGatewayVersion(PACKAGE_BINARY, resolveOptions("0.0.91"))).toMatchObject({
supported: false,
version: "0.0.91",
binaryPath: PACKAGE_BINARY,
message: expect.stringContaining("maximum 0.0.85"),
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

it("declines a package gateway older than the blueprint minimum", () => {
expect(checkUpstreamGatewayVersion(PACKAGE_BINARY, resolveOptions("0.0.71"))).toMatchObject({
supported: false,
message: expect.stringContaining("minimum 0.0.85"),
});
});

it("adopts a package gateway inside the supported window", () => {
expect(checkUpstreamGatewayVersion(PACKAGE_BINARY, resolveOptions("0.0.85")).supported).toBe(
true,
);
});

it("adopts the package gateway when its version cannot be determined", () => {
// Pre-#8094 behaviour: only decline on positive evidence of a bad version,
// so an unreadable binary never turns a working host into a failing one.
const verdict = checkUpstreamGatewayVersion(
PACKAGE_BINARY,
resolveOptions("ignored", { getUpstreamGatewayVersion: () => null }),
);

expect(verdict.supported).toBe(true);
});

it("adopts when the effective package gateway binary cannot be resolved", () => {
const verdict = checkUpstreamGatewayVersion(null, resolveOptions("0.0.91"));

expect(verdict.supported).toBe(true);
});

it("stops reporting a package unit whose gateway is out of window", () => {
const warn = vi.fn();

// The unit file is present, so the pre-fix resolver adopted it outright.
expect(hasOpenShellGatewayUserService(resolveOptions("0.0.91", { warn }))).toBe(false);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("0.0.91"));
});

it("keeps reporting a package unit whose gateway is in window", () => {
expect(hasOpenShellGatewayUserService(resolveOptions("0.0.85"))).toBe(true);
});

it("falls back to the NemoClaw-managed unit when the package gateway is rejected", () => {
const home = "/home/tester";
const nemoclawUnit = getNemoclawOpenShellGatewayUserServicePath(home, {});

const resolved = hasOpenShellGatewayUserService(
resolveOptions("0.0.91", {
home,
env: {},
existsSync: (p: string) => packageOnly(p) || p === nemoclawUnit,
lstatSync: (() => ({ isSymbolicLink: () => false })) as never,
readFileSync: () => NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE_MARKER_LINE,
}),
);

expect(resolved).toBe(true);
});

it("falls back to the NemoClaw-managed unit when the package service identity is untrusted", () => {
const home = "/home/tester";
const nemoclawUnit = getNemoclawOpenShellGatewayUserServicePath(home, {});
const getUpstreamGatewayVersion = vi.fn(() => "0.0.85");

const resolved = hasOpenShellGatewayUserService(
resolveOptions("ignored", {
home,
env: {},
existsSync: (p: string) => packageOnly(p) || p === nemoclawUnit,
getUpstreamGatewayVersion,
lstatSync: (() => ({ isSymbolicLink: () => false })) as never,
readFileSync: () => NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE_MARKER_LINE,
spawnSyncImpl: () => ({
status: 0,
stdout: trustedShowOutput("/opt/foreign/openshell-gateway"),
}),
}),
);

expect(resolved).toBe(true);
expect(getUpstreamGatewayVersion).not.toHaveBeenCalled();
});

it("warns once even when the resolver runs repeatedly", () => {
const warn = vi.fn();
const options = resolveOptions("0.0.91", { warn });

hasOpenShellGatewayUserService(options);
hasOpenShellGatewayUserService(options);
hasOpenShellGatewayUserService(options);

expect(warn).toHaveBeenCalledTimes(1);
});

it("probes every documented package binary location", () => {
const localBinary = "/usr/local/bin/openshell-gateway";
expect(checkUpstreamGatewayVersion(localBinary, resolveOptions("0.0.91"))).toMatchObject({
supported: false,
binaryPath: localBinary,
});
});

it("checks the effective ExecStart when both package binaries exist", () => {
const getUpstreamGatewayVersion = vi.fn(() => "0.0.91");

expect(
hasOpenShellGatewayUserService(
resolveOptions("ignored", {
existsSync: (p: string) =>
getOpenShellGatewayUserServicePaths().includes(p) ||
getOpenShellGatewayUserServiceBinaryPaths().includes(p),
getUpstreamGatewayVersion,
spawnSyncImpl: () => ({ status: 0, stdout: trustedShowOutput(PACKAGE_BINARY) }),
}),
),
).toBe(false);
expect(getUpstreamGatewayVersion).toHaveBeenCalledWith(PACKAGE_BINARY);
});

it("preserves above-maximum development gateways on the development channel", () => {
expect(
checkUpstreamGatewayVersion(
PACKAGE_BINARY,
resolveOptions("openshell-gateway 0.0.91-dev.1", {
env: { NEMOCLAW_OPENSHELL_CHANNEL: "dev" },
}),
).supported,
).toBe(true);
});

it("keeps the package unit paths the gate scans in sync with the resolver", () => {
expect(getOpenShellGatewayUserServicePaths()).toContain(PACKAGE_UNIT);
});
});
35 changes: 31 additions & 4 deletions src/lib/onboard/docker-driver-gateway-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,13 @@ describe("docker-driver-gateway-service", () => {
args[0] === "info" ? officialFormulaInfo() : spawnResult(),
);

expect(hasOpenShellGatewayUserService({ existsSync: linuxExists, platform: "linux" })).toBe(
true,
);
expect(
hasOpenShellGatewayUserService({
existsSync: linuxExists,
platform: "linux",
spawnSyncImpl: systemdSpawn([]),
}),
).toBe(true);
expect(
hasOpenShellGatewayUserService({
commandExists: (command) => command === "brew",
Expand Down Expand Up @@ -440,7 +444,6 @@ describe("docker-driver-gateway-service", () => {

it.each([
["the manager is unavailable", "daemon-reload", "Failed to connect to bus", false],
["the executable is foreign", "show", "", true],
["the service is inactive", "is-active", "inactive", false],
])("reports the selected systemd log command when %s (#8104)", (_case, failedCommand, detail, standaloneFallbackBlocked) => {
const result = startOpenShellGatewayUserService({
Expand Down Expand Up @@ -473,6 +476,30 @@ describe("docker-driver-gateway-service", () => {
});
});

it("declines an upstream systemd service with a foreign executable", () => {
const result = startOpenShellGatewayUserService({
commandExists: () => true,
env: {},
existsSync: (candidate) => candidate === "/lib/systemd/user/openshell-gateway.service",
platform: "linux",
spawnSyncImpl: () =>
spawnResult(
0,
"",
trustedShowOutput(
"/lib/systemd/user/openshell-gateway.service",
"/tmp/openshell-gateway",
),
),
});

expect(result).toMatchObject({
attempted: false,
reason: "service not installed",
started: false,
});
});

it("selects the managed service log command without service validation (#8104)", () => {
expect(getOpenShellGatewayManagedServiceLogCommand({ platform: "darwin" })).toContain(
"openshell-gateway.err.log",
Expand Down
Loading
Loading