Skip to content
11 changes: 11 additions & 0 deletions docs/manage-sandboxes/uninstall-nemoclaw.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ Rerun `NEMOCLAW_GATEWAY_PORT=<port> $$nemoclaw uninstall` with the gateway port
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.
Before scoped cleanup stops a Docker gateway process, including a managed default gateway service, NemoClaw requires two Docker namespace proofs.
The selected Docker gateway configuration and any running gateway process must use the state-root-specific OpenShell sandbox namespace that NemoClaw generated.
Because the supported OpenShell Podman schema does not expose `sandbox_namespace`, scoped Podman uninstall fails closed before signaling and preserves the gateway runtime evidence and local state.
Full single-gateway Podman uninstall continues to use normal graceful teardown.
For Docker, if either proof is absent, uninstall exits nonzero before it signals the host gateway.
NemoClaw preserves the gateway runtime evidence and local state.
Keep that state intact.
Restore the selected Docker gateway through the supported install or onboarding recovery flow so it restarts with the generated configuration.
Verify every gateway with `openshell gateway list`.
Retry the scoped uninstall.
Do not add `sandbox_namespace` manually to a live gateway configuration because the running process can still be using its previous namespace.

<Note>
In this section, `<selected-state-root>` is `~/.nemoclaw/` for the default gateway or `~/.nemoclaw/gateways/<port>/` for a non-default gateway.
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3892,6 +3892,17 @@ Rerun `NEMOCLAW_GATEWAY_PORT=<port> $$nemoclaw uninstall` with the gateway port
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.
Before scoped cleanup stops a Docker gateway process, including a managed default gateway service, NemoClaw requires two Docker namespace proofs.
The selected Docker gateway configuration and any running gateway process must use the state-root-specific OpenShell sandbox namespace that NemoClaw generated.
Because the supported OpenShell Podman schema does not expose `sandbox_namespace`, scoped Podman uninstall fails closed before signaling and preserves the gateway runtime evidence and local state.
Full single-gateway Podman uninstall continues to use normal graceful teardown.
For Docker, if either proof is absent, uninstall exits nonzero before it signals the host gateway.
NemoClaw preserves the gateway runtime evidence and local state.
Keep that state intact.
Restore the selected Docker gateway through the supported install or onboarding recovery flow so it restarts with the generated configuration.
Verify every gateway with `openshell gateway list`.
Retry the scoped uninstall.
Do not add `sandbox_namespace` manually to a live gateway configuration because the running process can still be using its previous namespace.

##### Uninstalling Every Gateway Port

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function ok(stdout = ""): RunResult {

function withManagedGatewayAuthority(deps: UninstallRunDeps): UninstallRunDeps {
return {
isPortFree: () => true,
resolveGatewayTeardownAuthority: ({ gatewayName, gatewayPort }) => ({
gatewayName,
gatewayPort,
Expand Down
46 changes: 36 additions & 10 deletions src/lib/actions/uninstall/run-plan-gateway-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import path from "node:path";

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

import { gatewayIdForStateDir } from "../../onboard/docker-driver-gateway-config";
import {
getNemoclawOpenShellGatewayUserServicePath,
getOpenShellUserConfigHome,
Expand Down Expand Up @@ -48,6 +49,7 @@ function fixture(useXdg = false): Fixture {
}

function writeManagedService(test: Fixture): string {
writeGatewayState(test);
const servicePath = getNemoclawOpenShellGatewayUserServicePath(test.home, test.env);
fs.mkdirSync(path.dirname(servicePath), { recursive: true });
fs.writeFileSync(
Expand Down Expand Up @@ -84,16 +86,13 @@ function writeSelectedSandboxRegistry(test: Fixture, sandboxName: string): strin
}

function writeGatewayState(test: Fixture): string {
const configPath = path.join(
test.home,
".local",
"state",
"nemoclaw",
"openshell-docker-gateway",
"openshell-gateway.toml",
);
const stateDir = path.join(test.home, ".local", "state", "nemoclaw", "openshell-docker-gateway");
const configPath = path.join(stateDir, "openshell-gateway.toml");
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, 'listen_address = "127.0.0.1:8080"\n');
fs.writeFileSync(
configPath,
`[openshell.drivers.docker]\nsandbox_namespace = "${gatewayIdForStateDir(stateDir)}"\n`,
);
return configPath;
}

Expand All @@ -109,6 +108,7 @@ function uninstall(
{
env: test.env,
existsSync: (target) => String(target).startsWith(test.root) && fs.existsSync(target),
isPortFree: () => true,
isTty: false,
platform: "linux",
resolveGatewayTeardownAuthority: ({ gatewayName, gatewayPort }) => ({
Expand All @@ -128,7 +128,9 @@ function uninstall(
run: (command, args, options) =>
command === "openshell" && args[0] === "gateway" && args[1] === "list"
? ok(JSON.stringify(gateways))
: run(command, args, options),
: command === "systemctl" && args.includes("--property=MainPID")
? ok("0\n")
: run(command, args, options),
},
);
}
Expand Down Expand Up @@ -245,6 +247,30 @@ describe("uninstall OpenShell gateway user service", () => {
expect(fs.existsSync(servicePath)).toBe(false);
});

it("does not signal a scoped service whose sandbox namespace is unproven (#8663)", () => {
const test = fixture(true);
const servicePath = writeManagedService(test);
fs.writeFileSync(writeGatewayState(test), "[openshell.drivers.docker]\n");
const calls: string[][] = [];

const result = uninstall(
test,
false,
{
commandExists: (command) => command === "systemctl",
run: (command, args) => {
calls.push([command, ...args]);
return ok();
},
},
[{ name: "nemoclaw" }, { name: "nemoclaw-8081" }],
);

expect(result.exitCode).toBe(1);
expect(fs.existsSync(servicePath)).toBe(true);
expect(calls.some(([command]) => command === "systemctl")).toBe(false);
});

it("preserves the marked Linux unit when scoped sandbox deletion fails (#8220)", () => {
const test = fixture(true);
const servicePath = writeManagedService(test);
Expand Down
69 changes: 64 additions & 5 deletions src/lib/actions/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import {
resolveGatewayTeardownAuthority,
} from "../../onboard/gateway-teardown-authority";
import {
hasStateScopedSandboxNamespace,
processUsesStateScopedSandboxNamespace,
type StopHostGatewayOptions,
stopHostGatewayProcesses,
} from "../../onboard/host-gateway-process";
Expand Down Expand Up @@ -98,12 +100,14 @@ export interface UninstallRunDeps {
error?: (message: string) => void;
existsSync?: (target: string) => boolean;
fs?: FileSystemDeps;
isPortFree?: (port: number) => boolean;
isTty?: boolean;
kill?: (pid: number, signal?: NodeJS.Signals | number) => boolean;
log?: (message: string) => void;
openRegularFile?: typeof openRegularFileNoFollow;
platform?: NodeJS.Platform;
readProcessArgv?: (pid: number) => readonly string[] | null;
readProcessEnvironment?: (pid: number) => Record<string, string> | null;
readLine?: () => string | null;
requireCompleteGatewayProcessCleanup?: boolean;
resolveGatewayTeardownAuthority?: GatewayTeardownAuthorityResolver;
Expand Down Expand Up @@ -406,12 +410,14 @@ interface UninstallRuntime {
env: NodeJS.ProcessEnv;
error: (message: string) => void;
existsSync: (target: string) => boolean;
isPortFree: ((port: number) => boolean) | undefined;
isTty: boolean;
kill: (pid: number, signal?: NodeJS.Signals | number) => boolean;
log: (message: string) => void;
openRegularFile: typeof openRegularFileNoFollow;
platform: NodeJS.Platform;
readProcessArgv: ((pid: number) => readonly string[] | null) | undefined;
readProcessEnvironment: ((pid: number) => Record<string, string> | null) | undefined;
readLine: () => string | null;
requireCompleteGatewayProcessCleanup: boolean;
resolveGatewayTeardownAuthority: GatewayTeardownAuthorityResolver;
Expand All @@ -432,6 +438,7 @@ function buildRuntime(deps: UninstallRunDeps): UninstallRuntime {
env,
error: deps.error ?? ((message) => console.error(message)),
existsSync: deps.existsSync ?? ((target) => fs.existsSync(target)),
isPortFree: deps.isPortFree,
// Side-effect-free TTY check + EAGAIN-tolerant reader; the
// process.stdin/non-blocking-fd hazard is documented in core/stdin.ts.
isTty: deps.isTty ?? isStdinTty(),
Expand All @@ -449,6 +456,7 @@ function buildRuntime(deps: UninstallRunDeps): UninstallRuntime {
openRegularFile: deps.openRegularFile ?? openRegularFileNoFollow,
platform: deps.platform ?? process.platform,
readProcessArgv: deps.readProcessArgv,
readProcessEnvironment: deps.readProcessEnvironment,
readLine: deps.readLine ?? readLineFromStdin,
requireCompleteGatewayProcessCleanup: deps.requireCompleteGatewayProcessCleanup ?? false,
resolveGatewayTeardownAuthority:
Expand Down Expand Up @@ -962,7 +970,10 @@ function stopOrphanedOpenShell(runtime: UninstallRuntime): void {
}
}

function removeNemoclawOpenShellGatewayUserService(runtime: UninstallRuntime): boolean {
function removeNemoclawOpenShellGatewayUserService(
runtime: UninstallRuntime,
scopedStateDir?: string,
): boolean {
if (runtime.platform !== "linux") return true;
const servicePath = getNemoclawOpenShellGatewayUserServicePath(
runtime.env.HOME || os.homedir(),
Expand Down Expand Up @@ -1000,6 +1011,33 @@ function removeNemoclawOpenShellGatewayUserService(runtime: UninstallRuntime): b

const hasSystemctl = runtime.commandExists("systemctl");
if (hasSystemctl) {
if (scopedStateDir !== undefined) {
const inspected = runtime.run(
"systemctl",
[
"--user",
"show",
NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE,
"--property=MainPID",
"--value",
],
{ env: runtime.env },
);
const mainPid = Number(inspected.stdout.trim());
if (
!hasStateScopedSandboxNamespace(scopedStateDir) ||
inspected.status !== 0 ||
!inspected.stdout.trim() ||
!Number.isSafeInteger(mainPid) ||
mainPid < 0 ||
(mainPid > 0 && !processUsesStateScopedSandboxNamespace(mainPid, scopedStateDir, runtime))
) {
runtime.warn(
"Refusing scoped gateway service stop because its loaded sandbox namespace cannot be proven.",
);
return false;
}
}
const disabled = runtime.run(
"systemctl",
["--user", "disable", "--now", NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE],
Expand Down Expand Up @@ -1043,10 +1081,13 @@ function removeManagedDefaultGatewayUserService(
runtime: UninstallRuntime,
options: UninstallRunOptions,
externallySupervised: boolean,
selectedStateDir?: string,
scoped = false,
): boolean {
return options.keepOpenShell || externallySupervised || GATEWAY_PORT !== DEFAULT_GATEWAY_PORT
? true
: removeNemoclawOpenShellGatewayUserService(runtime);
if (options.keepOpenShell || externallySupervised || GATEWAY_PORT !== DEFAULT_GATEWAY_PORT) {
return true;
}
return removeNemoclawOpenShellGatewayUserService(runtime, scoped ? selectedStateDir : undefined);
}

function removeNemoclawOpenShellGatewayEnv(
Expand Down Expand Up @@ -2099,7 +2140,15 @@ function executePlan(
return { ok: false };
}
if (scopedToSelectedGateway && !options.keepOpenShell && !externallySupervised) {
if (!removeManagedDefaultGatewayUserService(runtime, options, externallySupervised)) {
if (
!removeManagedDefaultGatewayUserService(
runtime,
options,
externallySupervised,
paths.selectedGatewayLocalStateDir,
true,
)
) {
return { ok: false };
}
stopHostGatewayProcessesForUninstall(runtime, {
Expand All @@ -2108,7 +2157,9 @@ function executePlan(
openShellGatewayName: options.gatewayName || resolveGatewayName(GATEWAY_PORT),
openShellGatewayPort: GATEWAY_PORT,
preserveRuntimeFilesOnNonMatching: true,
scopedGatewayStop: true,
stateDir: paths.selectedGatewayLocalStateDir,
usePgrepFallback: false,
});
} else if (scopedToSelectedGateway && externallySupervised) {
runtime.log("Kept the externally supervised OpenShell gateway process running.");
Expand Down Expand Up @@ -2258,9 +2309,17 @@ function stopHostGatewayProcessesForUninstall(
log: runtime.log,
warn: runtime.warn,
commandExists: runtime.commandExists,
isPortFree: runtime.isPortFree,
readProcessEnvironment: runtime.readProcessEnvironment,
},
options,
);
if (options.scopedGatewayStop && (result.ownershipFailures?.length || result.failed.length)) {
runtime.error(
"Cannot prove ownership of or stop the selected host gateway process; retaining its runtime evidence.",
);
throw new IncompleteHostGatewayCleanupError();
}
if (!runtime.requireCompleteGatewayProcessCleanup) return;
if (result.failed.length === 0 && result.orphanScanComplete !== false) return;
runtime.error("Cannot continue uninstall because host gateway process cleanup did not complete.");
Expand Down
58 changes: 55 additions & 3 deletions src/lib/onboard/docker-driver-gateway-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { randomBytes } from "node:crypto";
import { createHash, randomBytes } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import {
Expand All @@ -16,6 +16,7 @@ export { ensureDockerDriverGatewayJwtBundle } from "./docker-driver-gateway-jwt-
// See docs/security/openshell-0.0.72-compatibility-review.mdx for the source-of-truth review.
export const DOCKER_DRIVER_GATEWAY_CONFIG_NAME = "openshell-gateway.toml";
export const DOCKER_DRIVER_GATEWAY_JWT_TTL_SECS = 0;
export const NEMOCLAW_OPENSHELL_SANDBOX_NAMESPACE_ENV = "NEMOCLAW_OPENSHELL_SANDBOX_NAMESPACE";

function tomlString(value: string): string {
return JSON.stringify(value);
Expand Down Expand Up @@ -55,9 +56,54 @@ function cleanupStaleAtomicFileTemps(dir: string, basename: string): void {
}
}

function gatewayIdForStateDir(stateDir: string): string {
export function gatewayIdForStateDir(stateDir: string): string {
const leaf = path.basename(path.resolve(stateDir)).replace(/[^A-Za-z0-9_.-]/g, "-");
return leaf ? `nemoclaw-${leaf}` : "nemoclaw";
const scope = `${String(process.getuid?.() ?? "unknown")}\0${path.resolve(stateDir)}`;
const suffix = createHash("sha256").update(scope).digest("hex").slice(0, 12);
return `nemoclaw-${leaf || "gateway"}-${suffix}`;
}

/** Prove that a NemoClaw-owned Docker gateway config uses its state-scoped namespace. */
export function hasStateScopedSandboxNamespace(stateDir: string): boolean {
if (typeof process.getuid !== "function" || typeof fs.constants.O_NOFOLLOW !== "number") {
return false;
}
const configPath = path.join(stateDir, DOCKER_DRIVER_GATEWAY_CONFIG_NAME);
let descriptor: number | undefined;
try {
const state = fs.lstatSync(stateDir);
descriptor = fs.openSync(configPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
const config = fs.fstatSync(descriptor);
if (
!state.isDirectory() ||
state.isSymbolicLink() ||
!config.isFile() ||
config.nlink !== 1 ||
state.uid !== process.getuid() ||
config.uid !== state.uid ||
config.size > 64 * 1024
) {
return false;
}
const expected = `sandbox_namespace = ${tomlString(gatewayIdForStateDir(stateDir))}`;
let inDriverTable = false;
const matches = fs
.readFileSync(descriptor, "utf-8")
.split(/\r?\n/)
.filter((line) => {
const trimmed = line.trim();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
inDriverTable = trimmed === "[openshell.drivers.docker]";
return false;
}
return inDriverTable && trimmed.startsWith("sandbox_namespace =");
});
return matches.length === 1 && matches[0]?.trim() === expected;
} catch {
return false;
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
Comment on lines +66 to +106

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every producer of openshell-gateway.toml and of sandbox_namespace entries.
set -euo pipefail

rg -n --type=ts -C4 'DOCKER_DRIVER_GATEWAY_CONFIG_NAME|openshell-gateway\.toml'
rg -n --type=ts -C3 'sandbox_namespace'
rg -n --type=ts -C6 'function tomlString'

Repository: NVIDIA/NemoClaw

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd -i 'docker-driver-gateway-config|onboard' src/lib || true

printf '%s\n' '--- tracked references ---'
git ls-files | rg '(^|/)(docker-driver-gateway-config|onboard)([^/]*|/.*)$' || true
rg -n -S -C4 'DOCKER_DRIVER_GATEWAY_CONFIG_NAME|openshell-gateway\.toml|sandbox_namespace|writeDockerDriverGatewayConfig|tomlString' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/lib/onboard/docker-driver-gateway-config.ts"
printf '%s\n' '--- implementation outline ---'
ast-grep outline "$file" || true

printf '%s\n' '--- implementation ---'
cat -n "$file"

printf '%s\n' '--- exact symbol references ---'
rg -n --type=ts 'DOCKER_DRIVER_GATEWAY_CONFIG_NAME|writeDockerDriverGatewayConfig|hasStateScopedSandboxNamespace|gatewayIdForStateDir|sandbox_namespace' src/lib test src/commands 2>/dev/null || true

printf '%s\n' '--- config filename references outside TypeScript ---'
rg -n -S 'openshell-gateway\.toml|sandbox_namespace' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' --glob '!agents/**' 2>/dev/null || true

Repository: NVIDIA/NemoClaw

Length of output: 28818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- config and writer tests ---'
for file in \
  src/lib/onboard/docker-driver-gateway-config-toml.test.ts \
  src/lib/onboard/docker-driver-gateway-config-auth-contract.test.ts \
  src/lib/onboard/docker-driver-gateway-launch.test.ts \
  src/lib/onboard/docker-driver-gateway-cutover.ts \
  src/lib/onboard/docker-driver-gateway-compat.ts \
  src/lib/actions/uninstall/run-plan.ts
do
  if [ -f "$file" ]; then
    printf '\n### %s\n' "$file"
    wc -l "$file"
    rg -n -C5 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|OPENSHELL_GATEWAY_CONFIG|openshell-gateway\.toml|sandbox_namespace|upgrade|legacy|compat|cutover|rename|copyFile|writeFile' "$file" || true
  fi
done

printf '%s\n' '--- all config-builder and file-write call sites ---'
rg -n --type=ts -C3 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|writeFileSync|writeFileAtomic|copyFileSync|renameSync' src/lib/onboard src/lib/actions test/e2e 2>/dev/null | rg -C2 'GatewayConfig|gateway config|openshell-gateway|sandbox_namespace|writeDocker|buildDocker|copyFileSync|renameSync|writeFileSync' || true

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- direct call sites ---'
rg -n --type=ts 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|prepareDockerDriverGatewayConfigEnv' \
  src test --glob '!**/*.map' || true

printf '%s\n' '--- config TOML tests ---'
sed -n '1,260p' src/lib/onboard/docker-driver-gateway-config-toml.test.ts

printf '%s\n' '--- launch tests around namespace assertions ---'
sed -n '80,190p' src/lib/onboard/docker-driver-gateway-launch.test.ts

printf '%s\n' '--- cutover and compatibility source ---'
for file in src/lib/onboard/docker-driver-gateway-cutover.ts src/lib/onboard/docker-driver-gateway-compat.ts; do
  printf '\n### %s\n' "$file"
  cat -n "$file"
done

printf '%s\n' '--- migration references ---'
rg -n -i -C4 'docker.driver.gateway|gateway config|sandbox namespace|legacy identity|identity upgrade|migration|upgrade' \
  docs/security src/lib/onboard test/onboard-gateway-legacy-identity-upgrade-runtime.test.ts \
  --glob '!**/*.test.ts' 2>/dev/null || true

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact builder/writer call sites ---'
rg -n --type=ts 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|prepareDockerDriverGatewayConfigEnv' src test || true

printf '%s\n' '--- config TOML test assertions ---'
rg -n -C8 'sandbox_namespace|gatewayIdForStateDir|buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|legacy|upgrade' \
  src/lib/onboard/docker-driver-gateway-config-toml.test.ts || true

printf '%s\n' '--- launch test assertions ---'
rg -n -C8 'sandbox_namespace|gatewayIdForStateDir|buildDockerDriverGatewayConfigToml|legacy|upgrade' \
  src/lib/onboard/docker-driver-gateway-launch.test.ts || true

printf '%s\n' '--- compatibility implementation ---'
wc -l src/lib/onboard/docker-driver-gateway-compat.ts
cat -n src/lib/onboard/docker-driver-gateway-compat.ts

printf '%s\n' '--- gateway upgrade test references ---'
rg -n -C8 'legacy|upgrade|config|namespace|gatewayIdForStateDir|openshell-gateway' \
  test/onboard-gateway-legacy-identity-upgrade-runtime.test.ts src/lib/onboard/*gateway*test.ts 2>/dev/null | head -n 500 || true

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runtime config preparation ---'
sed -n '240,285p' src/lib/onboard/docker-driver-gateway-env.ts
sed -n '105,155p' src/lib/onboard/docker-driver-gateway-launch.ts
sed -n '1,80p' test/support/openshell-gateway-config-helpers.ts

printf '%s\n' '--- uninstall ownership gate and tests ---'
sed -n '1000,1050p' src/lib/actions/uninstall/run-plan.ts
rg -n -C10 'hasStateScopedSandboxNamespace|sandbox_namespace|legacy|unscoped|state-scoped' \
  src/lib/actions/uninstall src/lib/onboard/host-gateway-process-target.test.ts \
  src/lib/onboard/docker-driver-gateway-config-auth-contract.test.ts \
  src/lib/onboard/docker-driver-gateway-config-toml.test.ts || true

printf '%s\n' '--- OpenShell upgrade path ---'
rg -n -C8 'installOpenshell|needsUpgrade|upgrade|prepareDockerDriverGatewayConfigEnv|writeDockerDriverGatewayConfig' \
  src/lib/onboard/openshell-install.ts src/lib/onboard/openshell-pin.ts src/lib/onboard.ts \
  src/lib/onboard/docker-driver-gateway-env.ts src/lib/onboard/docker-driver-gateway-launch.ts || true

Repository: NVIDIA/NemoClaw

Length of output: 50371


Migrate legacy gateway TOML before scoped uninstall

openshell-install upgrades OpenShell without rewriting an existing openshell-gateway.toml. A legacy sandbox_namespace = "nemoclaw" therefore fails hasStateScopedSandboxNamespace, and scoped uninstall cannot stop the gateway. Add an idempotent migration or rewrite the file before uninstall. Add coverage for upgrade, restart, resume, and uninstall.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 89-90: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs
.readFileSync(descriptor, "utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/docker-driver-gateway-config.ts` around lines 66 - 106,
Update the onboarding upgrade/uninstall flow around
hasStateScopedSandboxNamespace to migrate or rewrite an existing legacy
openshell-gateway.toml so its Docker driver sandbox_namespace uses
gatewayIdForStateDir(stateDir) before scoped uninstall. Make the migration
idempotent and preserve valid scoped configurations, then add coverage for
upgrade, restart, resume, and uninstall paths.

}

function gatewayLocalTlsDir(gatewayEnv: Record<string, string>): string {
Expand All @@ -77,6 +123,7 @@ export function buildDockerDriverGatewayConfigToml(
const driver = gatewayEnv.OPENSHELL_DRIVERS === "podman" ? "podman" : "docker";
const localTlsDir = jwtBundle ? gatewayLocalTlsDir(gatewayEnv) : undefined;
const dockerEntries: [string, string | undefined][] = [
["sandbox_namespace", driver === "docker" ? gatewayId : undefined],
["grpc_endpoint", gatewayEnv.OPENSHELL_GRPC_ENDPOINT],
["host_gateway_ip", driver === "podman" ? PORTABLE_HOST_GATEWAY_IP : undefined],
["socket_path", driver === "podman" ? gatewayEnv.OPENSHELL_PODMAN_SOCKET : undefined],
Expand Down Expand Up @@ -169,5 +216,10 @@ export function prepareDockerDriverGatewayConfigEnv(
gatewayEnv,
sandboxBin,
);
if (gatewayEnv.OPENSHELL_DRIVERS === "podman") {
delete gatewayEnv[NEMOCLAW_OPENSHELL_SANDBOX_NAMESPACE_ENV];
} else {
gatewayEnv[NEMOCLAW_OPENSHELL_SANDBOX_NAMESPACE_ENV] = gatewayIdForStateDir(stateDir);
}
return gatewayEnv;
}
Loading
Loading