Skip to content
Merged
3 changes: 3 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3074,6 +3074,9 @@ Earlier releases only stopped `openshell forward` processes, so those orphans ac

For Local Ollama setups, uninstall also stops matching Ollama auth proxy processes before deleting `~/.nemoclaw` state so stale proxy listeners do not block a later reinstall.

For Hermes setups, uninstall inspects the selected gateway's managed port-forward watcher state, stops each verified watcher process and its sandbox-scoped forward, and leaves sibling gateway state untouched.
If any watcher or forward cleanup cannot be confirmed, uninstall exits nonzero and preserves the selected gateway's watcher state so you can retry cleanup.

On Linux, uninstall removes `~/.local/state/nemoclaw`, which contains Docker-driver gateway SQLite data, audit logs, VM-driver state, and standalone-fallback gateway PID files.

| Flag | Effect |
Expand Down
8 changes: 7 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ resolve_onboarded_agent() {
}

restore_onboard_forward_after_post_checks() {
local sandbox_name agent_name agent_display port openshell_bin attempt selected_state_dir state_dir pid_file watcher_script watcher_pid
local sandbox_name agent_name agent_display port openshell_bin openshell_dir attempt selected_state_dir state_dir pid_file watcher_script watcher_pid
sandbox_name="$(resolve_default_sandbox_name)"
agent_name="$(resolve_onboarded_agent)"
agent_display="$(agent_display_name "$agent_name")"
Expand All @@ -440,6 +440,12 @@ restore_onboard_forward_after_post_checks() {
else
return 0
fi
if [[ "$openshell_bin" != /* ]]; then
openshell_dir="${openshell_bin%/*}"
[[ "$openshell_dir" == "$openshell_bin" ]] && openshell_dir="."
openshell_dir="$(cd -- "$openshell_dir" && pwd -P)" || return 1
openshell_bin="${openshell_dir}/${openshell_bin##*/}"
fi

selected_state_dir="$(ensure_nemoclaw_state_dir)" || return 1
state_dir="${selected_state_dir}/state"
Expand Down
31 changes: 31 additions & 0 deletions src/lib/actions/uninstall/hermes-forward-watcher-cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
type HermesForwardWatcherHost,
stopHermesForwardWatcherProcess,
stopHermesSandboxForward,
} from "../../adapters/openshell/hermes-forward-watcher";
import { readHermesForwardWatcherState } from "../../state/hermes-forward-watcher";

export function stopHermesForwardWatchers(
nemoclawStateDir: string,
host: HermesForwardWatcherHost,
): boolean {
const state = readHermesForwardWatcherState(nemoclawStateDir);
if (!state.readable) {
host.warn(`Failed to inspect Hermes forward watcher state under ${nemoclawStateDir}.`);
return false;
}
if (state.watchers.length === 0) {
host.log("No Hermes forward watchers found");
return true;
}

let allStopped = true;
for (const watcher of state.watchers) {
if (!stopHermesForwardWatcherProcess(watcher, host)) allStopped = false;
if (!stopHermesSandboxForward(watcher, host)) allStopped = false;
}
return allStopped;
}
70 changes: 70 additions & 0 deletions src/lib/actions/uninstall/hermes-forward-watcher-installer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

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

const REPOSITORY_ROOT = path.resolve(import.meta.dirname, "../../../..");
const INSTALLER = path.join(REPOSITORY_ROOT, "scripts", "install.sh");

function writeExecutable(target: string, contents: string): void {
fs.writeFileSync(target, contents, { mode: 0o755 });
}

describe("Hermes forward watcher installer contract", () => {
it("gives the watcher an absolute OpenShell path for a relative override (#7163)", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemohermes-forward-relative-"));
try {
const fakeBin = path.join(tmp, "bin");
const stateDir = path.join(tmp, ".nemoclaw");
const watcherLog = path.join(tmp, "watcher.log");
const openshell = path.join(fakeBin, "openshell");
fs.mkdirSync(fakeBin, { recursive: true });
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(
path.join(stateDir, "onboard-session.json"),
JSON.stringify({ sandboxName: "created-by-onboard", agent: "hermes" }),
);
writeExecutable(openshell, "#!/usr/bin/env bash\nexit 0\n");
writeExecutable(
path.join(fakeBin, "node"),
`#!/usr/bin/env bash
if [ "\${1:-}" = "-e" ] && [[ "\${2:-}" == *"const { spawn }"* ]]; then
printf '%s\n' "$4" > "$WATCHER_LOG"
exit 0
fi
exec ${JSON.stringify(process.execPath)} "$@"
`,
);
for (const command of ["curl", "sleep"]) {
writeExecutable(path.join(fakeBin, command), "#!/usr/bin/env bash\nexit 0\n");
}
const relativeOpenshell = path.relative(REPOSITORY_ROOT, openshell);
const result = spawnSync(
"bash",
["-c", 'source "$INSTALLER" 2>/dev/null; restore_onboard_forward_after_post_checks'],
{
cwd: REPOSITORY_ROOT,
encoding: "utf-8",
env: {
...process.env,
HOME: tmp,
INSTALLER,
NEMOCLAW_OPENSHELL_BIN: relativeOpenshell,
PATH: `${fakeBin}:/usr/bin:/bin`,
WATCHER_LOG: watcherLog,
},
},
);

expect(result.status, result.stderr).toBe(0);
expect(fs.readFileSync(watcherLog, "utf-8").trim()).toBe(openshell);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
});
Loading
Loading