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
2 changes: 1 addition & 1 deletion docs/deployment/gateway-lifecycle-authority.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The declaration uses the following fields.
| `version` | Contract version. Only `1` is supported. |
| `mode` | `nemoclaw-managed` or `externally-supervised`. |
| `endpoint` | Exact bare origin for an externally supervised gateway. Use `http` or `https`, host `127.0.0.1` or `[::1]`, and the gateway port that this NemoClaw process uses. Credentials, DNS names such as `localhost`, paths, query strings, and fragments are rejected. |
| `stateDir` | Absolute external gateway state directory. Keep it outside NemoClaw-owned state paths such as `~/.local/state/nemoclaw`; uninstall removes those owned paths and does not otherwise target the declared external directory. For HTTPS, it contains the client trust bundle. |
| `stateDir` | Absolute external gateway state directory. Keep it outside NemoClaw-owned state paths such as `~/.local/state/nemoclaw`. While the external gateway process remains, uninstall preserves the selected local gateway state and does not otherwise target the declared external directory. For HTTPS, it contains the client trust bundle. |
| `supervisor` | Required for `externally-supervised` and rejected for `nemoclaw-managed`. `kind` is `systemd-system` or `systemd-user`. `serviceName` names one `.service` unit, and `execPath` is the absolute gateway executable path. |
| `requiredCapabilities` | Capabilities onboarding needs. Supported values are `gateway.health`, `sandbox.create`, `sandbox.exec`, and `gpu.passthrough`. |

Expand Down
5 changes: 3 additions & 2 deletions docs/manage-sandboxes/uninstall-nemoclaw.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ $$nemoclaw uninstall
| Flag | Effect |
|---|---|
| `--yes` | Skip the confirmation prompt. |
| `--keep-openshell` | Leave OpenShell binaries and NemoClaw-managed gateway service files in place, and do not stop the host gateway process. |
| `--keep-openshell` | Leave OpenShell binaries, NemoClaw-managed gateway service files, and local gateway state in place, and do not stop the host gateway process. |
| `--delete-models` | Also remove NemoClaw-pulled Ollama models. |
| `--destroy-user-data` | Also remove preserved user data in `rebuild-backups/`, `backups/`, and `sandboxes.json`. |
| `--gateway <name>` | Optional consistency check; must match the name derived from `NEMOCLAW_GATEWAY_PORT`. |
Expand All @@ -31,7 +31,8 @@ Do not use `--gateway` to select another instance; when supplied for compatibili
Default-port uninstall removes NemoClaw-managed entries in `openshell/gateway.env`.
For a NemoClaw-managed authority, it also removes only NemoClaw's marked Linux gateway unit.
It preserves upstream Linux package units, the macOS Homebrew service, and unrelated environment entries.
For an externally supervised authority, uninstall preserves the gateway process, supervisor resources, marked Linux unit, Docker resources, OpenShell binaries, and the declared external state directory.
For an externally supervised authority, uninstall preserves the local gateway state used by the running process in both full and gateway-scoped cleanup.
It also preserves the gateway process, supervisor resources, marked Linux unit, Docker resources, OpenShell binaries, and the declared external state directory.
A custom-port uninstall does not stop or remove the default gateway service or its environment file.

<Note>
Expand Down
9 changes: 6 additions & 3 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3369,14 +3369,16 @@ For Local Ollama setups, uninstall also stops matching Ollama auth proxy process
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 NemoClaw-owned Docker-driver gateway SQLite data, audit logs, VM-driver state, and standalone-fallback gateway PID files.
On Linux, uninstall removes `~/.local/state/nemoclaw` unless you pass `--keep-openshell` or the gateway is externally supervised.
That directory contains NemoClaw-owned Docker-driver gateway configuration and SQLite data, audit logs, VM-driver state, and standalone-fallback gateway PID files.
Uninstall preserves it when the managed or externally supervised gateway process remains because that process depends on the state.
Keep a declared external gateway state directory outside that NemoClaw-owned path.
Uninstall does not otherwise target the declared external directory.

| Flag | Effect |
|---|---|
| `--yes` | Skip the confirmation prompt |
| `--keep-openshell` | Leave OpenShell binaries and NemoClaw-managed gateway service files in place, and do not stop the host gateway process |
| `--keep-openshell` | Leave OpenShell binaries, NemoClaw-managed gateway service files, and local gateway state in place, and do not stop the host gateway process |
| `--delete-models` | Also remove NemoClaw-pulled Ollama models |
| `--destroy-user-data` | Also remove preserved user data (`rebuild-backups/`, `backups/`, `sandboxes.json`) |
| `--gateway <name>` | Optional consistency check; must match the name derived from `NEMOCLAW_GATEWAY_PORT` |
Expand All @@ -3392,7 +3394,8 @@ The compatibility `--gateway` flag cannot select another instance: when present,
Default-port uninstall removes NemoClaw-managed entries in `openshell/gateway.env`.
For a NemoClaw-managed authority, it also removes only NemoClaw's marked Linux gateway unit.
It preserves upstream Linux package units, the macOS Homebrew service, and unrelated environment entries.
For an externally supervised authority, uninstall preserves the gateway process, supervisor resources, marked Linux unit, Docker resources, OpenShell binaries, and the declared external state directory.
For an externally supervised authority, uninstall preserves the selected local gateway state in both full and gateway-scoped cleanup.
It also preserves the gateway process, supervisor resources, marked Linux unit, Docker resources, OpenShell binaries, and the declared external state directory.
A custom-port uninstall does not stop or remove the default gateway service or its environment file.

##### User-data preservation under `~/.nemoclaw/`
Expand Down
15 changes: 14 additions & 1 deletion src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,22 @@ describe("uninstall gateway-port segregation (#3053)", () => {
["full", "systemd-user"],
["scoped", "systemd-system"],
["scoped", "systemd-user"],
] as const)("preserves the gateway process, Docker resources, and OpenShell binaries during %s uninstall for a %s-supervised gateway (#6576)", (scope, kind) => {
] as const)("preserves the gateway process, Docker resources, OpenShell binaries, and gateway state during %s uninstall for a %s-supervised gateway (#6576)", (scope, kind) => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-external-"));
try {
const stateDir = path.join(tmpHome, ".nemoclaw");
const gatewayStatePath = path.join(
tmpHome,
".local",
"state",
"nemoclaw",
"openshell-docker-gateway",
"openshell-gateway.toml",
);
const gatewayState = 'listen_address = "127.0.0.1:8080"\n';
fs.mkdirSync(stateDir, { recursive: true });
fs.mkdirSync(path.dirname(gatewayStatePath), { recursive: true });
fs.writeFileSync(gatewayStatePath, gatewayState);
const prepareScope = {
full: () => undefined,
scoped: () =>
Expand Down Expand Up @@ -129,6 +140,8 @@ describe("uninstall gateway-port segregation (#3053)", () => {
command === "rm" && args.includes("/usr/local/bin/openshell-gateway"),
),
).toBe(false);
expect(fs.existsSync(gatewayStatePath)).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(fs.readFileSync(gatewayStatePath, "utf8")).toBe(gatewayState);
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
Expand Down
79 changes: 75 additions & 4 deletions src/lib/actions/uninstall/run-plan-gateway-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE_MARKER,
} from "../../onboard/docker-driver-gateway-service";
import { HOST_GATEWAY_PGREP_PATTERN } from "../../onboard/host-gateway-process";
import { type RunResult, type UninstallRunDeps, runUninstallPlan } from "./run-plan";
import { type RunResult, runUninstallPlan, type UninstallRunDeps } from "./run-plan";

function ok(stdout = ""): RunResult {
return { status: 0, stdout, stderr: "" };
Expand Down Expand Up @@ -68,7 +68,26 @@ function writeGatewayEnv(test: Fixture, contents = "OPENSHELL_SERVER_PORT=8080\n
return envPath;
}

function uninstall(test: Fixture, keepOpenShell: boolean, deps: Partial<UninstallRunDeps> = {}) {
function writeGatewayState(test: Fixture): string {
const configPath = path.join(
test.home,
".local",
"state",
"nemoclaw",
"openshell-docker-gateway",
"openshell-gateway.toml",
);
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, 'listen_address = "127.0.0.1:8080"\n');
return configPath;
}

function uninstall(
test: Fixture,
keepOpenShell: boolean,
deps: Partial<UninstallRunDeps> = {},
gateways: { name: string }[] = [{ name: "nemoclaw" }],
) {
const { commandExists = () => false, run = () => ok(), ...overrides } = deps;
return runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell },
Expand All @@ -93,32 +112,83 @@ function uninstall(test: Fixture, keepOpenShell: boolean, deps: Partial<Uninstal
commandExists: (command) => command === "openshell" || commandExists(command),
run: (command, args, options) =>
command === "openshell" && args[0] === "gateway" && args[1] === "list"
? ok(JSON.stringify([{ name: "nemoclaw" }]))
? ok(JSON.stringify(gateways))
: run(command, args, options),
},
);
}

describe("uninstall OpenShell gateway user service", () => {
it("keeps the service, env, and gateway process with --keep-openshell (#6903)", () => {
it("keeps the service, env, gateway process, and state with --keep-openshell (#7830)", () => {
const test = fixture(true);
const servicePath = writeManagedService(test);
const envPath = writeGatewayEnv(test);
const gatewayStatePath = writeGatewayState(test);
const run = vi.fn((_command: string, _args: string[]) => ok());

expect(uninstall(test, true, { commandExists: () => true, run }).exitCode).toBe(0);
expect(fs.existsSync(servicePath)).toBe(true);
expect(fs.existsSync(envPath)).toBe(true);
expect(fs.existsSync(gatewayStatePath)).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(run.mock.calls.map(([, args]) => args)).not.toContainEqual([
"-f",
HOST_GATEWAY_PGREP_PATTERN,
]);
});

it("keeps selected gateway state when sibling gateways require scoped cleanup (#7830)", () => {
const test = fixture(true);
const servicePath = writeManagedService(test);
const envPath = writeGatewayEnv(test);
const gatewayStatePath = writeGatewayState(test);

const result = uninstall(test, true, { commandExists: () => true }, [
{ name: "nemoclaw" },
{ name: "sibling" },
]);

expect(result.exitCode).toBe(0);
expect(fs.existsSync(servicePath)).toBe(true);
expect(fs.existsSync(envPath)).toBe(true);
expect(fs.existsSync(gatewayStatePath)).toBe(true);
});

it("keeps selected gateway state during scoped cleanup under external supervision (#6576)", () => {
const test = fixture(true);
const gatewayStatePath = writeGatewayState(test);

const result = uninstall(
test,
false,
{
commandExists: () => true,
resolveGatewayTeardownAuthority: ({ gatewayName, gatewayPort }) => ({
gatewayName,
gatewayPort,
mode: "externally-supervised",
source: "declared",
endpoint: `http://127.0.0.1:${String(gatewayPort)}`,
stateDir: path.dirname(gatewayStatePath),
supervisor: {
kind: "systemd-user",
serviceName: "external-openshell.service",
execPath: "/usr/local/bin/openshell-gateway",
},
requiredCapabilities: [],
}),
},
[{ name: "nemoclaw" }, { name: "sibling" }],
);

expect(result.exitCode).toBe(0);
expect(fs.existsSync(gatewayStatePath)).toBe(true);
});

it("removes only the marked Linux unit and managed env on full uninstall (#6903)", () => {
const test = fixture(true);
const servicePath = writeManagedService(test);
const envPath = writeGatewayEnv(test);
const gatewayStatePath = writeGatewayState(test);
const calls: string[][] = [];

const result = uninstall(test, false, {
Expand All @@ -132,6 +202,7 @@ describe("uninstall OpenShell gateway user service", () => {
expect(result.exitCode).toBe(0);
expect(fs.existsSync(servicePath)).toBe(false);
expect(fs.existsSync(envPath)).toBe(false);
expect(fs.existsSync(gatewayStatePath)).toBe(false);
expect(calls).toContainEqual([
"systemctl",
"--user",
Expand Down
29 changes: 24 additions & 5 deletions src/lib/actions/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1719,7 +1719,14 @@ function executePlan(
) {
return { ok: false };
}
removePath(paths.selectedGatewayLocalStateDir, runtime);
if (!options.keepOpenShell && !externallySupervised)
removePath(paths.selectedGatewayLocalStateDir, runtime);
else
runtime.log(
externallySupervised
? "Keeping OpenShell gateway configuration used by the externally supervised gateway."
: "Keeping OpenShell gateway configuration as requested.",
);
runtime.log(
"Legacy sibling gateway rows remain; kept the shared default-root state for their recovery.",
);
Expand Down Expand Up @@ -1748,16 +1755,28 @@ function executePlan(
)
ok = false;
if (scopedToSelectedGateway) {
removePath(paths.selectedGatewayLocalStateDir, runtime);
if (!options.keepOpenShell && !externallySupervised)
removePath(paths.selectedGatewayLocalStateDir, runtime);
else
runtime.log(
externallySupervised
? "Keeping OpenShell gateway configuration used by the externally supervised gateway."
: "Keeping OpenShell gateway configuration as requested.",
);
if (GATEWAY_PORT === DEFAULT_GATEWAY_PORT && !options.keepOpenShell) {
const envCleanup = removeNemoclawOpenShellGatewayEnv(paths, runtime);
if (!envCleanup.ok) ok = false;
}
runtime.log("Sibling gateways remain; kept shared OpenShell and NemoClaw config.");
} else {
removePath(paths.gatewayLocalStateDir, runtime);
if (options.keepOpenShell)
runtime.log("Keeping OpenShell gateway configuration as requested.");
if (!options.keepOpenShell && !externallySupervised)
removePath(paths.gatewayLocalStateDir, runtime);
if (options.keepOpenShell || externallySupervised)
runtime.log(
externallySupervised
? "Keeping OpenShell gateway configuration used by the externally supervised gateway."
: "Keeping OpenShell gateway configuration as requested.",
);
else if (GATEWAY_PORT === DEFAULT_GATEWAY_PORT) {
const envCleanup = removeNemoclawOpenShellGatewayEnv(paths, runtime);
if (!envCleanup.ok) ok = false;
Expand Down
Loading