Skip to content
7 changes: 5 additions & 2 deletions docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ graph LR
The logical diagram above shows how components relate.
This section shows what actually runs where on the host.
NemoClaw's default Docker-driver topology does not place the sandbox in an embedded k3s cluster.
On Linux and Apple Silicon macOS, NemoClaw starts the OpenShell Docker-driver gateway and creates the sandbox as a Docker container.
The gateway normally runs as a host process; Linux hosts that need the gateway compatibility patch may run the same gateway binary inside a small container.
On Linux, NemoClaw configures and restarts the package-managed OpenShell gateway user service when it is installed, then creates the sandbox as a Docker container.
NemoClaw treats that service as authoritative only when `systemctl --user show openshell-gateway` reports a package/vendor unit path and an `openshell-gateway` `ExecStart`.
Per-user units, partial units, and user-manager or bus outages do not take over gateway ownership; NemoClaw falls back to the standalone gateway process used by earlier installs.
That compatibility fallback remains until supported upgrade paths no longer include pre-service OpenShell installs and the package-managed handoff has direct nightly coverage.
On Apple Silicon macOS, NemoClaw starts the OpenShell Docker-driver gateway and creates the sandbox as a Docker container.
In both Docker-driver modes, the sandbox is a Docker container, not a Kubernetes pod.
Legacy non-Docker-driver installs still use the k3s-based gateway path; the diagram below shows the standard Docker-driver topology.

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ 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.

On Linux, uninstall removes `~/.local/state/nemoclaw`, which contains Docker-driver gateway PID files, SQLite data, audit logs, and VM-driver state.
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 Expand Up @@ -1418,9 +1418,9 @@ These flags toggle optional behaviors during onboarding; set them before running
| `NEMOCLAW_SANDBOX_GPU` | `auto`, `1`, or `0` | Controls sandbox GPU passthrough during onboarding. `auto` enables GPU passthrough when an NVIDIA GPU is detected, `1` requires GPU passthrough, and `0` forces CPU-only sandbox creation. |
| `NEMOCLAW_SANDBOX_GPU_DEVICE` | OpenShell GPU device selector | Selects the GPU device passed with `openshell sandbox create --gpu-device`. Requires explicit sandbox GPU enablement with `NEMOCLAW_SANDBOX_GPU=1` (or `--sandbox-gpu` for CLI-driven onboarding); otherwise onboarding rejects the selector instead of treating it as an implicit opt-in. |
| `NEMOCLAW_DOCKER_GPU_PATCH` | `0` to disable, anything else to keep the default | Controls the Linux Docker-driver GPU sandbox compatibility patch. Set to `0` only as an escape hatch when the patch fails and you need onboarding to continue without patching the GPU sandbox container. |
| `NEMOCLAW_OPENSHELL_GATEWAY_BIN` | path | Advanced override for the `openshell-gateway` binary used by the Linux Docker-driver gateway. Defaults to the binary next to `openshell`, then common install paths. |
| `NEMOCLAW_OPENSHELL_SANDBOX_BIN` | path | Advanced override for the `openshell-sandbox` binary passed to the Linux Docker-driver gateway supervisor. Defaults to the binary next to `openshell`, then common install paths. |
| `NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR` | path | Advanced override for the Linux Docker-driver gateway pid file and SQLite state directory. Defaults to `~/.local/state/nemoclaw/openshell-docker-gateway`. |
| `NEMOCLAW_OPENSHELL_GATEWAY_BIN` | path | Advanced override for the `openshell-gateway` binary used by the Linux Docker-driver standalone fallback. Defaults to the binary next to `openshell`, then common install paths. |
| `NEMOCLAW_OPENSHELL_SANDBOX_BIN` | path | Advanced override for the `openshell-sandbox` binary used by the Linux Docker-driver standalone fallback. Defaults to the binary next to `openshell`, then common install paths. |
| `NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR` | path | Advanced override for the Linux Docker-driver gateway SQLite state directory and standalone-fallback PID file. Defaults to `~/.local/state/nemoclaw/openshell-docker-gateway`. |
| `NEMOCLAW_WECHAT_QUIET` | `1` to enable | Silences the `[wechat]` diagnostic lines printed during the host-side WeChat QR login (poll status, IDC redirects, swallowed gateway errors), which are visible by default while the experimental WeChat path stabilizes; set `1` once the flow is reliable in your environment. |

### Onboard Profiling Traces
Expand Down
10 changes: 5 additions & 5 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,7 @@ const { isGatewayTcpReady } =
require("./onboard/gateway-tcp-readiness") as typeof import("./onboard/gateway-tcp-readiness");
const { trackChildExit } =
require("./onboard/child-exit-tracker") as typeof import("./onboard/child-exit-tracker");
const { reportDockerDriverGatewayStartFailure } =
require("./onboard/docker-driver-gateway-failure") as typeof import("./onboard/docker-driver-gateway-failure");
const { reportDockerDriverGatewayStartFailure } = require("./onboard/docker-driver-gateway-failure") as typeof import("./onboard/docker-driver-gateway-failure");
const dockerDriverGatewayEnv: typeof import("./onboard/docker-driver-gateway-env") =
require("./onboard/docker-driver-gateway-env");
const { getDockerDriverGatewayEndpoint } = dockerDriverGatewayEnv;
Expand Down Expand Up @@ -2410,20 +2409,21 @@ async function startGatewayWithOptions(
}

async function startDockerDriverGateway({ exitOnFailure = true, skipSandboxBridgeReachability = false }: { exitOnFailure?: boolean; skipSandboxBridgeReachability?: boolean } = {}): Promise<void> {
dockerDriverGatewayEnv.writeDockerGatewayDebEnvOverride(() => getDockerDriverGatewayEnv());
const gatewayBin = resolveOpenShellGatewayBinary();
const openshellVersionOutput = runCaptureOpenshell(["--version"], {
ignoreError: true,
});
const gatewayEnv = getDockerDriverGatewayEnv(openshellVersionOutput);
dockerDriverGatewayEnv.writeDockerGatewayDebEnvOverride(() => gatewayEnv);
const stateDir = getDockerDriverGatewayStateDir();
const runtimeIdentity = gatewayBin ? dockerDriverGatewayLaunch.buildDockerDriverGatewayRuntimeIdentity({ gatewayBin, gatewayEnv, stateDir, sandboxBin: resolveOpenShellSandboxBinary() }) : null;
const gatewayLaunch = runtimeIdentity?.launch ?? null;
const driftGatewayBin = runtimeIdentity?.driftGatewayBin ?? gatewayBin;
const driftGatewayEnv = runtimeIdentity?.desiredEnv ?? gatewayEnv;
const identityGatewayBin = runtimeIdentity?.identityGatewayBin ?? gatewayBin;
const { verifySandboxBridgeGatewayReachableOrExit } =
require("./onboard/gateway-sandbox-reachability") as typeof import("./onboard/gateway-sandbox-reachability");
const { verifySandboxBridgeGatewayReachableOrExit } = require("./onboard/gateway-sandbox-reachability") as typeof import("./onboard/gateway-sandbox-reachability");

if (await dockerDriverGatewayEnv.startPackageManagedDockerDriverGateway({ clearDockerDriverGatewayRuntimeFiles, exitOnFailure, gatewayName: GATEWAY_NAME, registerDockerDriverGatewayEndpoint, runCaptureOpenshell, skipSandboxBridgeReachability, verifySandboxBridgeGatewayReachableOrExit })) return;

const gatewayStatus = runCaptureOpenshell(["status"], { ignoreError: true });
const gwInfo = runCaptureOpenshell(["gateway", "info", "-g", GATEWAY_NAME], {
Expand Down
31 changes: 28 additions & 3 deletions src/lib/onboard/docker-driver-gateway-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,16 @@ describe("writeDockerGatewayDebEnvOverride", () => {

const existsSpy = vi
.spyOn(fs, "existsSync")
.mockImplementation((candidate) => candidate === "/usr/bin/openshell-gateway");
.mockImplementation((candidate) => candidate === "/usr/lib/systemd/user/openshell-gateway.service");
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(tempHome);

try {
writeDockerGatewayDebEnvOverride(() => ({
const wrote = writeDockerGatewayDebEnvOverride(() => ({
OPENSHELL_BIND_ADDRESS: "127.0.0.1",
}));
}), { platform: "linux" });

const envFileContent = fs.readFileSync(envFile, "utf-8");
expect(wrote).toBe(true);
expect(fs.statSync(envDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(envFile).mode & 0o777).toBe(0o600);
expect(envFileContent).toContain("KEEP_ME=1\n");
Expand All @@ -146,4 +147,28 @@ describe("writeDockerGatewayDebEnvOverride", () => {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});

it("does not write service env for standalone gateway binaries", () => {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-env-"));
const existsSpy = vi
.spyOn(fs, "existsSync")
.mockImplementation((candidate) => candidate === "/usr/bin/openshell-gateway");
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(tempHome);

try {
const wrote = writeDockerGatewayDebEnvOverride(
() => ({
OPENSHELL_BIND_ADDRESS: "127.0.0.1",
}),
{ platform: "linux" },
);

expect(wrote).toBe(false);
expect(fs.existsSync(path.join(tempHome, ".config", "openshell", "gateway.env"))).toBe(false);
} finally {
existsSpy.mockRestore();
homedirSpy.mockRestore();
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
});
13 changes: 6 additions & 7 deletions src/lib/onboard/docker-driver-gateway-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
getGatewayHttpsEndpoint,
} from "../core/gateway-address";
import { GATEWAY_PORT } from "../core/ports";
import { hasOpenShellGatewayUserService } from "./docker-driver-gateway-service";

export { getGatewayHttpsEndpoint };
export { startPackageManagedDockerDriverGateway } from "./docker-driver-gateway-service";

export const DOCKER_DRIVER_GATEWAY_RUNTIME_ENV_KEYS = [
"OPENSHELL_DRIVERS",
Expand Down Expand Up @@ -133,13 +135,9 @@ function readTextFileIfPresent(filePath: string): string {

export function writeDockerGatewayDebEnvOverride(
getOverride: () => Record<string, string>,
): void {
const servicePaths = [
"/usr/bin/openshell-gateway",
"/usr/lib/systemd/user/openshell-gateway.service",
"/lib/systemd/user/openshell-gateway.service",
];
if (!servicePaths.some((candidate) => fs.existsSync(candidate))) return;
opts: Parameters<typeof hasOpenShellGatewayUserService>[0] = {},
): boolean {
if (!hasOpenShellGatewayUserService(opts)) return false;
const override = getOverride();
const envDir = path.join(os.homedir(), ".config", "openshell");
const envFile = path.join(envDir, "gateway.env");
Expand All @@ -151,4 +149,5 @@ export function writeDockerGatewayDebEnvOverride(
mode: 0o600,
});
fs.chmodSync(envFile, 0o600);
return true;
}
Loading
Loading