Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 26 additions & 7 deletions src/lib/onboard/openshell-pin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,31 @@ export type RunOpenshellInstallDeps = OpenshellInstallPinDeps & {
setOpenshellBin: (binPath: string | null) => void;
};

export type PrependInstalledUserLocalOpenshellPathDeps = {
env?: NodeJS.ProcessEnv;
getFutureShellPathHint: RunOpenshellInstallDeps["getFutureShellPathHint"];
};

/** Keep the installed user-local OpenShell directory first across separate NemoClaw command processes. */
export function prependInstalledUserLocalOpenshellPath(
deps: PrependInstalledUserLocalOpenshellPathDeps,
): string | null {
const env = deps.env ?? process.env;
const localBin = env.XDG_BIN_HOME || path.join(env.HOME || "", ".local", "bin");
const openshellPath = path.join(localBin, "openshell");
try {
if (!fs.statSync(openshellPath).isFile()) return null;
fs.accessSync(openshellPath, fs.constants.X_OK);
} catch {
return null;
}
const futureShellPathHint = deps.getFutureShellPathHint(localBin, env.PATH ?? "");
if (futureShellPathHint !== null) {
env.PATH = env.PATH ? `${localBin}${path.delimiter}${env.PATH}` : localBin;
}
return futureShellPathHint;
}

Comment thread
prekshivyas marked this conversation as resolved.
/**
* Execute `scripts/install-openshell.sh`, wiring in the blueprint-driven pin
* resolution and the host-side state updates onboard.ts cares about (binary
Expand Down Expand Up @@ -234,13 +259,7 @@ export function runOpenshellInstall(deps: RunOpenshellInstallDeps): OpenShellIns
return { installed: false, localBin: null, futureShellPathHint: null };
}
const localBin = process.env.XDG_BIN_HOME || path.join(process.env.HOME || "", ".local", "bin");
const openshellPath = path.join(localBin, "openshell");
const futureShellPathHint = fs.existsSync(openshellPath)
? deps.getFutureShellPathHint(localBin, process.env.PATH)
: null;
if (fs.existsSync(openshellPath) && futureShellPathHint) {
process.env.PATH = `${localBin}${path.delimiter}${process.env.PATH}`;
}
const futureShellPathHint = prependInstalledUserLocalOpenshellPath(deps);
const bin = deps.resolveOpenshell();
deps.setOpenshellBin(bin);
if (bin) process.env.NEMOCLAW_OPENSHELL_BIN = bin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
type ContainerEngine,
type ContainerEngineCommandCapture,
} from "../../adapters/container-engine";
import { prependInstalledUserLocalOpenshellPath } from "../openshell-pin";
import { getFutureShellPathHint } from "../remediation";
import {
createDockerLlamaCppManagedLifecycle,
type DockerLlamaCppManagedLifecycle,
Expand Down Expand Up @@ -58,8 +60,13 @@ export function createDockerLlamaCppOperationAuthority(
capture?: ContainerEngineCommandCapture,
spawnCommand?: HostLocalInferenceCommandSpawner,
): DockerLlamaCppOperationAuthority {
const operationEnv = { ...env };
prependInstalledUserLocalOpenshellPath({
env: operationEnv,
getFutureShellPathHint,
});
const authority = withManagedLlamaCppError(() =>
createDockerOperationAuthority("host-local-inference", env, capture),
createDockerOperationAuthority("host-local-inference", operationEnv, capture),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
const assertAuthority = () => withManagedLlamaCppError(authority.assertAuthority);
return Object.freeze({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createContextCapture as contextCapture,
createDriftingContextCapture,
} from "../../../../test/helpers/docker-operation-authority-test-helpers";
import { prependInstalledUserLocalOpenshellPath } from "../openshell-pin";
import { createDockerLlamaCppOperationAuthority } from "./docker-llama-cpp-operation";
import {
createDockerOperationAuthority,
Expand Down Expand Up @@ -138,6 +139,65 @@ describe("Docker operation authority", () => {
expect(second.engine.authorityId).not.toBe(first.engine.authorityId);
});

it("keeps managed llama.cpp Docker authority after onboarding resumes (#9585)", () => {
const executableRoot = fakeDocker("qualified");
const home = fakeExecutableRoot();
const localBin = path.join(home, ".local", "bin");
fs.mkdirSync(localBin, { recursive: true });
writeFakeExecutable(localBin, "openshell", "printf 'openshell\\n'");
const common = {
HOME: home,
DOCKER_HOST: "unix:///tmp/nemoclaw-docker.sock",
};
const installed = createDockerLlamaCppOperationAuthority({
...common,
PATH: `${localBin}${path.delimiter}${executableRoot}`,
});
const resumedEnvironment = {
...common,
PATH: executableRoot,
};
const resumed = createDockerLlamaCppOperationAuthority(resumedEnvironment);

expect(resumedEnvironment.PATH).toBe(executableRoot);
expect(resumed.engine.authorityId).toBe(installed.engine.authorityId);
expect(dockerOperationBindingSha256(resumed.engine)).toBe(
dockerOperationBindingSha256(installed.engine),
);
});

it("does not trust a non-executable user-local OpenShell path (#9585)", () => {
const executableRoot = fakeDocker("qualified");
const home = fakeExecutableRoot();
const localBin = path.join(home, ".local", "bin");
fs.mkdirSync(localBin, { recursive: true });
fs.writeFileSync(path.join(localBin, "openshell"), "not executable\n", { mode: 0o600 });
const environment = {
HOME: home,
DOCKER_HOST: "unix:///tmp/nemoclaw-docker.sock",
PATH: executableRoot,
};
const baseline = createDockerOperationAuthority("host-local-inference", environment);
const managed = createDockerLlamaCppOperationAuthority(environment);

expect(managed.engine.authorityId).toBe(baseline.engine.authorityId);
expect(environment.PATH).toBe(executableRoot);
});

it("does not trust a user-local OpenShell directory (#9585)", () => {
const home = fakeExecutableRoot();
const localBin = path.join(home, ".local", "bin");
fs.mkdirSync(path.join(localBin, "openshell"), { recursive: true });
const environment = { HOME: home, PATH: "/usr/bin" };
const getFutureShellPathHint = vi.fn(() => "export PATH");

expect(
prependInstalledUserLocalOpenshellPath({ env: environment, getFutureShellPathHint }),
).toBeNull();
expect(getFutureShellPathHint).not.toHaveBeenCalled();
expect(environment.PATH).toBe("/usr/bin");
});

it("keeps authority stable across SSH session metadata and does not forward it", () => {
const executableRoot = fakeDockerScript(
`printf '%s\\n' "\${XDG_SESSION_ID-unset}" "\${XDG_SESSION_CLASS-unset}" "\${XDG_SESSION_TYPE-unset}"`,
Expand Down
Loading