Skip to content

Commit 8ff35b3

Browse files
committed
fix(uninstall): keep shared shims when sibling ports remain
Scoped --destroy-user-data must not drop ~/.local/bin managed shims when otherGatewayPorts proves a real sibling still shares them. Limit removal to the unidentified scoped path and cover the confirmed-sibling retention case. Signed-off-by: Rui Luo <ruluo@nvidia.com>
1 parent 2610352 commit 8ff35b3

5 files changed

Lines changed: 46 additions & 16 deletions

File tree

docs/manage-sandboxes/uninstall-nemoclaw.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ It preserves resources from a separate OpenClaw installation while continuing to
2525
| `--yes` | Skip the confirmation prompt. |
2626
| `--keep-openshell` | Leave OpenShell binaries, NemoClaw-managed gateway service files, and local gateway state in place, and do not stop the host gateway process. |
2727
| `--delete-models` | Delete every model reported by the host's local Ollama inventory and all non-credential data in the current user's shared `~/.cache/huggingface/` cache after managed model runtimes stop. Hugging Face authentication files remain. |
28-
| `--destroy-user-data` | Also remove preserved user data in `rebuild-backups/`, `backups/`, and `sandboxes.json`, and remove installer-managed user-local CLI shims under `~/.local/bin/`. When sibling gateways remain, the shared npm CLI package stays. |
28+
| `--destroy-user-data` | Also remove preserved user data in `rebuild-backups/`, `backups/`, and `sandboxes.json`. Removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified. When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package. |
2929
| `--all-gateway-ports` | Uninstall every gateway port on the host instead of only the selected one. |
3030
| `--gateway <name>` | Optional consistency check; must match the name derived from `NEMOCLAW_GATEWAY_PORT`. |
3131

@@ -230,7 +230,7 @@ If validation, runtime cleanup, or temporary-claim retirement fails, uninstall e
230230
Resolve the reported SSH, Docker, peer-host, or claim error before you retry.
231231
Runtime cleanup can partially complete before an error, so inspect every cluster node before retrying.
232232
When sibling gateways remain, it removes only the selected gateway's resources and port-scoped state and preserves those shared host resources.
233-
With `--destroy-user-data`, that scoped path still removes installer-managed user-local CLI shims under `~/.local/bin/` while it keeps the shared npm CLI package and the other shared host resources for remaining gateways.
233+
With `--destroy-user-data`, that scoped path removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified (for example odd `~/.nemoclaw/gateways/` entries or an unreadable gateway list). When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package and the other shared host resources.
234234
A recorded sandbox that OpenShell reports as already removed leaves nothing to delete, so the scoped path reports it, finishes the remaining gateway cleanup, and exits `0`.
235235
A sandbox that OpenShell cannot reach or refuses to delete still leaves state behind.
236236
Uninstall exits nonzero and preserves the selected gateway registration, port-scoped state, and selected sandbox registry entry for a retry.

docs/reference/commands.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4346,7 +4346,7 @@ Uninstall does not otherwise target the declared external directory.
43464346
| `--yes` | Skip the confirmation prompt |
43474347
| `--keep-openshell` | Leave OpenShell binaries, NemoClaw-managed gateway service files, and local gateway state in place, and do not stop the host gateway process |
43484348
| `--delete-models` | Outside portable cleanup, delete every model reported by the host's local Ollama inventory and all non-credential data in the current user's shared `~/.cache/huggingface/` cache after managed model runtimes stop. Hugging Face authentication files remain. Portable cleanup preserves both model stores and every Podman image. |
4349-
| `--destroy-user-data` | Also remove preserved user data (`rebuild-backups/`, `backups/`, `sandboxes.json`) and installer-managed user-local CLI shims under `~/.local/bin/`. When sibling gateways remain, the shared npm CLI package stays. Portable cleanup still retains its recovery record until later completed onboarding durably supersedes it. |
4349+
| `--destroy-user-data` | Also remove preserved user data (`rebuild-backups/`, `backups/`, `sandboxes.json`). Removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified. When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package. Portable cleanup still retains its recovery record until later completed onboarding durably supersedes it. |
43504350
| `--all-gateway-ports` | Uninstall every gateway port on the host, not only the port `NEMOCLAW_GATEWAY_PORT` selects |
43514351
| `--gateway <name>` | Optional consistency check; must match the name derived from `NEMOCLAW_GATEWAY_PORT` |
43524352

@@ -4452,7 +4452,7 @@ The default gateway uses `~/.nemoclaw/`; a non-default gateway uses `~/.nemoclaw
44524452

44534453
Outside portable cleanup, when uninstall confirms that no sibling gateways remain, it also removes shared host resources such as the gateway source clone, runtime state, and the Ollama auth proxy PID file.
44544454
When sibling gateways remain, it removes only the selected gateway's resources and port-scoped state while preserving those shared host resources.
4455-
With `--destroy-user-data`, that scoped path still removes installer-managed user-local CLI shims under `~/.local/bin/` while it keeps the shared npm CLI package and the other shared host resources for remaining gateways.
4455+
With `--destroy-user-data`, that scoped path removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified (for example odd `~/.nemoclaw/gateways/` entries or an unreadable gateway list). When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package and the other shared host resources.
44564456
If the OpenShell command is unavailable or its gateway list cannot be read, uninstall cannot confirm that the selected gateway is the last one, so it uses the same scoped path and preserves the shared resources.
44574457
When the command itself is unavailable, uninstall exits nonzero before OpenShell cleanup so you can restore the command and retry.
44584458

@@ -4468,8 +4468,8 @@ Decision matrix:
44684468
| Interactive TTY, preserved entries present, no env override | Prompts `Also remove them? [y/N]`. Default `N` keeps the entries. |
44694469
| Interactive TTY, user answers `y` | Removes the preserved entries in the selected gateway's state root; outside portable cleanup, a single-gateway uninstall also removes the remaining shared state. Portable cleanup preserves shared OpenShell resources and its retirement record. |
44704470
| Non-interactive (`--yes`, `NEMOCLAW_NON_INTERACTIVE=1`, or non-TTY shell) | Preserves the entries and prints a one-line notice. |
4471-
| `--destroy-user-data` | Skips the secondary user-data prompt, removes the preserved entries in the selected gateway's state root, and removes installer-managed user-local CLI shims under `~/.local/bin/`. When sibling gateways remain, the shared npm CLI package stays. The global `Proceed?` confirmation still applies unless `--yes` is also passed. |
4472-
| `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1` | Skips the secondary user-data prompt, removes the preserved entries, and removes installer-managed user-local CLI shims under `~/.local/bin/`. When sibling gateways remain, the shared npm CLI package stays. The global `Proceed?` confirmation still applies unless `--yes` is also passed. |
4471+
| `--destroy-user-data` | Skips the secondary user-data prompt and removes the preserved entries in the selected gateway's state root. Removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified. When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package. The global `Proceed?` confirmation still applies unless `--yes` is also passed. |
4472+
| `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1` | Skips the secondary user-data prompt and removes the preserved entries. Removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified. When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package. The global `Proceed?` confirmation still applies unless `--yes` is also passed. |
44734473

44744474
The preserved entries survive uninstall as inert files on disk.
44754475
Reinstall NemoClaw and re-onboard the sandbox before `$$nemoclaw <name> snapshot restore` can use them.
@@ -5145,7 +5145,7 @@ The following flags change defaults for commands that manage existing sandboxes.
51455145
| `NEMOCLAW_SHIELDS_SETTLE_MS` | milliseconds (default `750`, clamped to `0` to `10000`) | Settle window NemoClaw waits after re-applying a config lockdown (during shields auto-restore and `$$nemoclaw <name> shields up` drift remediation) before re-confirming the lock still holds. Detects when an in-sandbox reconciler changes config file permissions after lockdown and re-applies the lock; if NemoClaw cannot re-confirm the lock within the retry budget, shields stay down. This narrows the window in which a reconciler can revert permissions rather than eliminating it. The best-effort `chattr +i` immutable bit remains the only fully durable lock. Raise it on hosts where the gateway settles slowly. |
51465146
| `NEMOCLAW_SKIP_UNREACHABLE_SANDBOX_BACKUP` | Exactly `1` to opt in (`true`, `yes`, `0` are not accepted) | Applies to standalone `$$nemoclaw backup-all` runs. Skips running sandboxes whose in-sandbox SSH endpoint does not answer. It does not relax the installer's strict pre-upgrade backup, which still aborts if any registered sandbox is skipped or fails. Any uncommitted state since the last successful backup is not included in the skipped backup. |
51475147
| `NEMOCLAW_UNINSTALL_ALL_GATEWAY_PORTS` | `1` to opt in | Makes `$$nemoclaw uninstall` remove every gateway port on the host instead of only the port `NEMOCLAW_GATEWAY_PORT` selects. Equivalent to passing the `--all-gateway-ports` flag; the whole-host `Proceed?` confirmation still applies unless `--yes` is also passed. Each port runs as its own uninstall, and the variable is dropped from those runs so the sweep cannot re-enter itself. |
5148-
| `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA` | `1` to opt in | Acknowledges data loss during `$$nemoclaw uninstall`, removes the otherwise-preserved entries (`rebuild-backups/`, `backups/`, `sandboxes.json`) under `~/.nemoclaw/`, and removes installer-managed user-local CLI shims under `~/.local/bin/`. When sibling gateways remain, the shared npm CLI package stays. Equivalent to passing the `--destroy-user-data` flag; the global `Proceed?` confirmation still applies unless `--yes` is also passed. |
5148+
| `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA` | `1` to opt in | Acknowledges data loss during `$$nemoclaw uninstall` and removes the otherwise-preserved entries (`rebuild-backups/`, `backups/`, `sandboxes.json`) under `~/.nemoclaw/`. Removes installer-managed user-local CLI shims under `~/.local/bin/` only when sibling evidence is unidentified. When a confirmed sibling gateway port remains, those shared shims stay with the shared npm CLI package. Equivalent to passing the `--destroy-user-data` flag; the global `Proceed?` confirmation still applies unless `--yes` is also passed. |
51495149

51505150
<AgentOnly variant="openclaw">
51515151
### Deprecated Brev Deployment

src/lib/actions/root-help.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function help(): void {
8585
` --delete-models${" ".repeat(19)}Remove all Ollama models and non-credential Hugging Face cache data (authentication files remain)`,
8686
);
8787
lines.push(
88-
` --destroy-user-data${" ".repeat(15)}Remove preserved ~/.nemoclaw/ user data and managed ~/.local/bin CLI shims`,
88+
` --destroy-user-data${" ".repeat(15)}Remove preserved ~/.nemoclaw/ user data; managed CLI shims unless a confirmed sibling remains`,
8989
);
9090
lines.push(
9191
` --all-gateway-ports${" ".repeat(15)}Uninstall every gateway port on this host, not only NEMOCLAW_GATEWAY_PORT`,

src/lib/actions/uninstall/run-plan-gateway-scan-entries.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ function okWithKnownGatewayList(command: string, args: readonly string[]): RunRe
5555
: ok();
5656
}
5757

58+
function okWithSiblingGatewayList(command: string, args: readonly string[]): RunResult {
59+
return command === "openshell" && args[0] === "gateway" && args[1] === "list"
60+
? ok(JSON.stringify([{ name: "nemoclaw" }, { name: "nemoclaw-9123" }]))
61+
: ok();
62+
}
63+
5864
const SCOPED_RETENTION_LOG =
5965
"Sibling gateways remain; kept the shared NemoClaw CLI and shell shims.";
6066
const SCOPED_PACKAGE_RETENTION_LOG =
@@ -126,7 +132,9 @@ function writeScopedGatewayState(home: string, port = 8080): void {
126132
function uninstall(
127133
home: string,
128134
shims: readonly string[],
129-
options: Pick<UninstallRunOptions, "destroyUserData"> = {},
135+
options: Pick<UninstallRunOptions, "destroyUserData"> & {
136+
run?: (command: string, args: readonly string[]) => RunResult;
137+
} = {},
130138
) {
131139
const logs: string[] = [];
132140
const result = runUninstallPlan(
@@ -147,7 +155,7 @@ function uninstall(
147155
rmSync: vi.fn((target: fs.PathLike, rmOptions?: fs.RmOptions) => {
148156
String(target).startsWith(home) ? fs.rmSync(target, rmOptions) : undefined;
149157
}),
150-
run: vi.fn(okWithKnownGatewayList),
158+
run: vi.fn(options.run ?? okWithKnownGatewayList),
151159
runDocker: () => ok(""),
152160
},
153161
);
@@ -264,4 +272,24 @@ describe("uninstall gateway-directory scan", () => {
264272
fs.rmSync(home, { recursive: true, force: true });
265273
}
266274
});
275+
276+
it("keeps managed CLI shims with --destroy-user-data when a confirmed sibling gateway remains (#9277)", () => {
277+
const { home, shims } = makeHome("nemoclaw-uninstall-destroy-sibling-", []);
278+
writeScopedGatewayState(home);
279+
280+
try {
281+
const { result, logs, survivors } = uninstall(home, shims, {
282+
destroyUserData: true,
283+
run: okWithSiblingGatewayList,
284+
});
285+
286+
expect(result.exitCode).toBe(0);
287+
expect(logs).toContain(SCOPED_RETENTION_LOG);
288+
expect(logs).not.toContain(DESTROY_SHIM_CLEANUP_LOG);
289+
expect(logs).not.toContain(SCOPED_PACKAGE_RETENTION_LOG);
290+
expect(survivors).toEqual(shims);
291+
} finally {
292+
fs.rmSync(home, { recursive: true, force: true });
293+
}
294+
});
267295
});

src/lib/actions/uninstall/run-plan.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,6 +2617,7 @@ function executePlan(
26172617
preserveUnderStateDir: readonly string[],
26182618
scopedToSelectedGateway: boolean,
26192619
sharedRegistryMustBePreserved: boolean,
2620+
otherGatewayPorts: readonly number[],
26202621
sandboxNames: readonly string[],
26212622
teardownAuthority: GatewayOwner,
26222623
portableRuntimeCleanup: boolean,
@@ -2759,12 +2760,12 @@ function executePlan(
27592760
}
27602761
} else if (step.name === "NemoClaw CLI") {
27612762
if (scopedToSelectedGateway) {
2762-
// Sibling-scoped cleanup keeps the shared npm CLI package. With
2763-
// `--destroy-user-data`, also delete installer-managed user-local shims
2764-
// when `removePath` succeeds so a destructive uninstall does not leave
2765-
// them on PATH (#9277). Foreign files of those names stay; the summary
2766-
// log runs only when at least one managed shim was removed.
2767-
if (options.destroyUserData) {
2763+
// Confirmed sibling gateway ports share ~/.local/bin shims. Only the
2764+
// unidentified / unproven scoped path (#9277 false positives: odd
2765+
// gateways/ entries, unreadable gateway list, etc.) may drop managed
2766+
// shims on `--destroy-user-data` while keeping the shared npm package.
2767+
const confirmedSiblingPortsRemain = otherGatewayPorts.length > 0;
2768+
if (options.destroyUserData && !confirmedSiblingPortsRemain) {
27682769
runtime.log("Sibling gateways remain; kept the shared NemoClaw CLI package.");
27692770
const removedShims = removeManagedCliShims(paths, runtime);
27702771
if (removedShims > 0) {
@@ -3082,6 +3083,7 @@ export function runUninstallPlan(
30823083
preserveUnderStateDir,
30833084
scopedToSelectedGateway,
30843085
gatewayInspection.sharedRegistryMustBePreserved,
3086+
gatewayInspection.otherGatewayPorts,
30853087
sandboxNames,
30863088
teardownAuthority,
30873089
portableRuntimeCleanup,

0 commit comments

Comments
 (0)