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
24 changes: 22 additions & 2 deletions src/lib/actions/sandbox/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { loadAgent } from "../../agent/defs";
import { compareChannelSets, probeChannelRuntimeStatus } from "../../channel-runtime-status";
import { CLI_DISPLAY_NAME, CLI_NAME } from "../../cli/branding";
import { recoverNamedGatewayRuntime } from "../../gateway-runtime-action";
import { isLinuxDockerDriverGatewayEnabled } from "../../onboard/docker-driver-platform";
import { executeSandboxCommandForVerification } from "../../onboard/sandbox-verification-exec";
import { readCloudflaredState } from "../../tunnel/services";
import { probeProviderHealth, type ProviderHealthStatus } from "../../inference/health";
Expand Down Expand Up @@ -205,7 +206,7 @@ function dockerInspectGateway(containerName: string): DoctorCheck[] {
label: "Docker container",
status: "fail",
detail: `${containerName} not found or not inspectable`,
hint: "run `docker ps --filter name=openshell-cluster-nemoclaw`",
hint: `run \`docker ps --filter name=${containerName}\``,
});
return checks;
}
Expand Down Expand Up @@ -514,6 +515,23 @@ function messagingDoctorCheck(sandboxName: string, sb: SandboxEntry): DoctorChec
};
}

/**
* Decide whether to inspect the legacy k3s gateway container
* (`openshell-cluster-<name>`). That container only exists for the legacy
* Kubernetes gateway driver. The current Linux/arm64 Docker-driver gateway runs
* as a host process (or a separate `nemoclaw-openshell-gateway` compatibility
* container), so inspecting `openshell-cluster-nemoclaw` there always fails and
* produces a false doctor failure even when OpenShell reports the named gateway
* as connected (#4502). Prefer the sandbox's recorded driver; fall back to
* platform detection for older registry entries that predate the field.
*/
function shouldInspectLegacyGatewayContainer(sb: SandboxEntry | null | undefined): boolean {
const driver = sb?.openshellDriver;
if (driver === "docker" || driver === "vm") return false;
if (driver === "kubernetes") return true;
return !isLinuxDockerDriverGatewayEnabled();
}

type RunSandboxDoctorOptions = {
quietJson?: boolean;
};
Expand Down Expand Up @@ -576,7 +594,9 @@ export async function runSandboxDoctor(
hint: openshellBin ? undefined : "install OpenShell before using sandbox commands",
});

checks.push(...dockerInspectGateway(`openshell-cluster-${NEMOCLAW_GATEWAY_NAME}`));
if (shouldInspectLegacyGatewayContainer(sb)) {
checks.push(...dockerInspectGateway(`openshell-cluster-${NEMOCLAW_GATEWAY_NAME}`));
}

let openshellConnected = false;
if (openshellBin) {
Expand Down
85 changes: 85 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2739,6 +2739,91 @@ describe("CLI dispatch", () => {
);
});

it("doctor does not inspect the legacy k3s gateway container in Docker-driver mode", () => {
const setup = createDoctorTestSetup("nemoclaw-cli-doctor-docker-driver-", [
'case "$*" in',
' "status") printf "Server Status\\n\\n Gateway: nemoclaw\\n Status: Connected\\n"; exit 0 ;;',
' "gateway info -g nemoclaw") printf "Gateway: nemoclaw\\n"; exit 0 ;;',
' "sandbox list") printf "NAME STATUS\\nalpha Ready\\n"; exit 0 ;;',
' "inference get") printf "Provider: nvidia-prod\\nModel: test-model\\n"; exit 0 ;;',
"esac",
]);
// Docker-driver sandbox: no legacy `openshell-cluster-*` container exists.
writeSandboxRegistry(setup.home, "alpha", { openshellDriver: "docker" });
// Record docker argv and make `docker inspect` fail like an absent legacy
// container would. The doctor must not even attempt the inspect, so this
// should never produce a failure — and we assert the call was skipped, not
// merely that its failure was tolerated.
const dockerCalls = path.join(setup.home, "docker-calls");
fs.writeFileSync(
path.join(setup.localBin, "docker"),
[
"#!/usr/bin/env bash",
`printf '%s\\n' "$*" >> ${JSON.stringify(dockerCalls)}`,
'if [ "$1" = "info" ]; then echo "24.0.0"; exit 0; fi',
'if [ "$1" = "inspect" ]; then echo "Error: No such object: $3" >&2; exit 1; fi',
"exit 0",
].join("\n"),
{ mode: 0o755 },
);
// Healthy curl so the unrelated provider-health probe does not fail the
// report and mask the gateway-only assertions below.
fs.writeFileSync(
path.join(setup.localBin, "curl"),
["#!/usr/bin/env bash", 'echo "{}"', "exit 0"].join("\n"),
{ mode: 0o755 },
);

const r = setup.runDoctor("alpha doctor --json");

expect(r.out).not.toContain("openshell-cluster");
const report = JSON.parse(r.out) as {
status: string;
checks: Array<{ group: string; label: string; status: string; detail: string }>;
};
expect(report.checks.find((check) => check.label === "Docker container")).toBeUndefined();
// Core contract: the legacy k3s container inspect must be skipped entirely,
// not attempted-and-ignored.
const recordedDockerCalls = fs.existsSync(dockerCalls)
? fs.readFileSync(dockerCalls, "utf8")
: "";
expect(recordedDockerCalls).not.toMatch(/\binspect\b/);
const openshellStatus = report.checks.find((check) => check.label === "OpenShell status");
expect(openshellStatus).toEqual(
expect.objectContaining({ group: "Gateway", status: "ok", detail: "connected to nemoclaw" }),
);
// The Docker-driver gateway is healthy, so no Gateway check should fail.
expect(report.checks.filter((c) => c.group === "Gateway" && c.status === "fail")).toEqual([]);
expect(report.status).toBe("ok");
expect(r.code).toBe(0);
});

it("doctor still inspects the legacy k3s gateway container for the kubernetes driver", () => {
const setup = createDoctorTestSetup("nemoclaw-cli-doctor-k8s-driver-", [
'case "$*" in',
' "status") printf "Server Status\\n\\n Gateway: nemoclaw\\n Status: Connected\\n"; exit 0 ;;',
' "gateway info -g nemoclaw") printf "Gateway: nemoclaw\\n"; exit 0 ;;',
' "sandbox list") printf "NAME STATUS\\nalpha Ready\\n"; exit 0 ;;',
' "inference get") printf "Provider: nvidia-prod\\nModel: test-model\\n"; exit 0 ;;',
"esac",
]);
writeSandboxRegistry(setup.home, "alpha", { openshellDriver: "kubernetes" });

const r = setup.runDoctor("alpha doctor --json");

const report = JSON.parse(r.out) as {
checks: Array<{ group: string; label: string; status: string; detail: string }>;
};
const dockerContainer = report.checks.find((check) => check.label === "Docker container");
expect(dockerContainer).toEqual(
expect.objectContaining({
group: "Gateway",
status: "ok",
detail: expect.stringContaining("openshell-cluster-nemoclaw"),
}),
);
});

it(
"doctor reports fresh shields state as not configured instead of down",
testTimeoutOptions(30_000),
Expand Down
Loading