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
18 changes: 18 additions & 0 deletions docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2939,6 +2939,24 @@ Podman is not a tested runtime.
OpenShell officially documents Docker-based runtimes only.
If you encounter issues with Podman, switch to a tested runtime (Docker Engine, Docker Desktop, or Colima) and rerun onboarding.

The portable experimental profile uses the `docker` command to drive rootless Podman.
Before you run this profile, make sure a Docker-compatible CLI is available on `PATH`.
On a Podman-only host, install the `podman-docker` shim for your distribution:

```bash
# Debian or Ubuntu
sudo apt install podman-docker

# Fedora
sudo dnf install podman-docker
```

Then rerun portable onboarding:

```bash
$$nemoclaw onboard --experimental-profile portable
```

<AgentOnly variant="hermes">

## Hermes
Expand Down
61 changes: 54 additions & 7 deletions src/lib/onboard/experimental/portable-host-preparation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ describe("preparePortableExperimentalHost", () => {
);
const docker = vi
.fn<(args: readonly string[], env: NodeJS.ProcessEnv) => SpawnResult>()
.mockReturnValueOnce(result(1))
.mockReturnValueOnce(result());
.mockReturnValueOnce(result()) // --version probe: docker-compatible CLI present
.mockReturnValueOnce(result(1)) // inspect: registry not present yet
.mockReturnValueOnce(result()); // run
const podman = vi.fn(() => result(0, "/run/user/1001/custom/podman.sock\n"));
const env: NodeJS.ProcessEnv = {
NEMOCLAW_EXPERIMENTAL_PROFILE: "portable",
Expand Down Expand Up @@ -75,7 +76,8 @@ describe("preparePortableExperimentalHost", () => {
["--user", "enable", "--now", "podman.socket"],
]);
expect(podman).toHaveBeenCalledWith(["info", "--format", "{{.Host.RemoteSocket.Path}}"], env);
expect(docker.mock.calls[1]?.[0]).toEqual([
expect(docker.mock.calls[0]?.[0]).toEqual(["--version"]);
expect(docker.mock.calls[2]?.[0]).toEqual([
"run",
"-d",
"--name",
Expand Down Expand Up @@ -109,8 +111,9 @@ describe("preparePortableExperimentalHost", () => {
);
const docker = vi
.fn<(args: readonly string[], env: NodeJS.ProcessEnv) => SpawnResult>()
.mockReturnValueOnce(result(1))
.mockReturnValueOnce(result());
.mockReturnValueOnce(result()) // --version probe: docker-compatible CLI present
.mockReturnValueOnce(result(1)) // inspect: registry not present yet
.mockReturnValueOnce(result()); // run
const podman = vi.fn(() => result(0, "/run/user/1001/podman/podman.sock\n"));

preparePortableExperimentalHost(
Expand Down Expand Up @@ -151,7 +154,7 @@ describe("preparePortableExperimentalHost", () => {
const timeout = Object.assign(new Error("registry inspection timed out"), {
code: "ETIMEDOUT",
});
const docker = vi.fn(
const docker = vi.fn<(args: readonly string[], env: NodeJS.ProcessEnv) => SpawnResult>(
() =>
({
error: timeout,
Expand All @@ -177,7 +180,8 @@ describe("preparePortableExperimentalHost", () => {
},
),
).toThrow(/Inspecting the managed portable registry failed: registry inspection timed out/);
expect(docker).toHaveBeenCalledTimes(1);
// The --version probe tolerates a non-ENOENT error, then the inspect fails.
expect(docker).toHaveBeenCalledTimes(2);
});

it("fails closed when Podman does not report an absolute local socket", () => {
Expand All @@ -198,4 +202,47 @@ describe("preparePortableExperimentalHost", () => {
),
).toThrow(/invalid socket path/);
});

it("names podman-docker and creates the registry only after a successful retry (#8453)", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-portable-"));
tempDirs.push(home);
// The `docker --version` probe returns a spawn ENOENT, i.e. no docker CLI.
const docker = vi
.fn<(args: readonly string[], env: NodeJS.ProcessEnv) => SpawnResult>()
.mockReturnValueOnce({
error: Object.assign(new Error("spawnSync docker ENOENT"), { code: "ENOENT" }),
output: [null, "", ""],
pid: 0,
signal: null,
status: null,
stderr: "",
stdout: "",
} as SpawnResult)
.mockReturnValueOnce(result()) // retry probe: podman-docker is now present
.mockReturnValueOnce(result(1)) // inspect: registry was not created by the failed attempt
.mockReturnValueOnce(result()); // run
const env: NodeJS.ProcessEnv = { NEMOCLAW_EXPERIMENTAL_PROFILE: "portable" };
const deps = {
platform: "linux" as const,
home,
uid: 1001,
systemctl: () => result(),
podman: () => result(0, "/run/user/1001/podman/podman.sock"),
docker,
};

expect(() => preparePortableExperimentalHost(env, deps)).toThrow(/podman-docker/);
// Fails on the CLI probe, before any registry inspect/run is attempted.
expect(docker).toHaveBeenCalledTimes(1);
expect(docker.mock.calls[0]?.[0]).toEqual(["--version"]);

preparePortableExperimentalHost(env, deps);

expect(docker.mock.calls.map(([args]) => args[0])).toEqual([
"--version",
"--version",
"inspect",
"run",
]);
});
});
23 changes: 23 additions & 0 deletions src/lib/onboard/experimental/portable-host-preparation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ function requireCommand(result: SpawnResult, description: string): void {
throw new Error(`${description} failed: ${commandDetail(result)}`);
}

/**
* The portable profile points DOCKER_HOST at the rootless Podman socket but still
* drives the managed registry — and the rest of onboarding's runtime preflight —
* through the `docker` CLI. On a genuinely Podman-only host that CLI is absent,
* so the first docker spawn fails with a cryptic `spawnSync docker ENOENT`
* instead of an actionable message (#8453). Detect that up front and tell the
* user to install the docker-compatible shim the profile expects.
*/
function requireDockerCompatibleCli(
docker: NonNullable<PortableHostPreparationDeps["docker"]>,
env: NodeJS.ProcessEnv,
): void {
const probe = docker(["--version"], env);
if ((probe.error as NodeJS.ErrnoException | undefined)?.code !== "ENOENT") return;
throw new Error(
"The portable experimental profile drives Podman through a docker-compatible CLI, but no " +
"`docker` command was found on PATH. On a Podman-only host, install the podman-docker shim " +
"(Debian/Ubuntu: `sudo apt install podman-docker`; Fedora: `sudo dnf install podman-docker`), " +
"then rerun `nemoclaw onboard --experimental-profile portable`.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}

function resolvePodmanDockerHost(result: SpawnResult): string {
requireCommand(result, "Resolving the rootless Podman API socket");
const socket = String(result.stdout ?? "").trim();
Expand Down Expand Up @@ -214,6 +236,7 @@ export function preparePortableExperimentalHost(
env: childEnv,
timeout: REGISTRY_COMMAND_TIMEOUT_MS,
}));
requireDockerCompatibleCli(docker, env);
ensureRegistryContainer(env, docker);
}

Expand Down
Loading