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
8 changes: 5 additions & 3 deletions docs/inference/set-up-ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ NemoClaw detects Ollama on the host and can install, start, or upgrade it on sup

## Install or Upgrade Ollama

If Ollama is installed but not started, NemoClaw starts it for you.
During onboarding, if Ollama is installed but not started, NemoClaw starts it for you.
On macOS and Linux, the wizard can offer to install Ollama when it is not present.

When either the Ollama CLI or running daemon is below the minimum version for the starter models, currently `0.7.0`, the wizard displays an explicit **Upgrade Ollama** entry.
Expand Down Expand Up @@ -57,10 +57,12 @@ For ordinary user-local installs, restart the daemon manually after a reboot.
For a sandbox created with the portable experimental profile, `$$nemoclaw <name> connect --probe-only` and `$$nemoclaw <name> recover` can restart the user-local daemon that NemoClaw installed.
Before it decides whether to start Ollama, recovery probes `http://127.0.0.1:11434/api/tags`.
If the API is unhealthy, recovery starts Ollama only when the sandbox records `ollama-local` and a valid ownership receipt names the fixed regular executable.
NemoClaw releases that predate this receipt do not claim an existing executable after an upgrade.
To authorize recovery for a previous NemoClaw user-local install, first verify that `${HOME}/.local/bin/ollama` is the executable you want NemoClaw to manage, then run `NEMOCLAW_PORTABLE_OLLAMA_REENROLL=1 nemoclaw <name> recover` once.
The command rejects a symlink or non-executable file before it records ownership.
It refuses to launch a duplicate when another `ollama` process exists but the API is unhealthy.
After it launches the daemon, it waits up to 30 seconds for `/api/tags` to return valid JSON with a `models` array.
Recovery then requires a trusted reachable result from the sandbox `inference.local` route.
When route repair needs the local Ollama dependency check, it verifies or recovers the authenticated proxy before it retries `inference.local`.
Recovery then verifies the authenticated proxy and requires HTTP 2xx from the sandbox `inference.local/v1/models` route.
If Ollama does not become healthy within 30 seconds, the command identifies the exact receipt-bound executable and its `serve` argument, then tells you to retry recovery.
The command exits non-zero with recovery guidance when startup or route validation fails.
It does not start, stop, or replace a system service or another user-managed Ollama daemon.
Expand Down
10 changes: 7 additions & 3 deletions docs/manage-sandboxes/recover-rebuild-sandboxes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,17 @@ Other workflows, including onboarding, rebuild, and `doctor --fix`, can explicit
For a portable experimental-profile sandbox with the recorded `ollama-local` provider, `connect --probe-only` and `recover` also verify the host-side inference chain.
Before it decides whether to start Ollama, the command probes `http://127.0.0.1:11434/api/tags` and leaves a healthy daemon unchanged.
When that API is unhealthy, it starts the fixed user-local executable only if NemoClaw has a valid ownership receipt.
NemoClaw releases that predate this receipt do not claim an existing executable after an upgrade.
To authorize recovery for a previous NemoClaw user-local install, first verify that `${HOME}/.local/bin/ollama` is the executable you want NemoClaw to manage.
Then run `NEMOCLAW_PORTABLE_OLLAMA_REENROLL=1 nemoclaw <name> recover` once.
The command rejects a symbolic link or non-executable file before it records ownership.
It refuses to launch a duplicate when another `ollama` process exists but the API remains unhealthy.
After a launch, recovery waits up to 30 seconds for `/api/tags` to return valid JSON with a `models` array.
It does not take over a system service or an unrelated user-managed daemon.
It refuses a symbolic link, non-regular file, or non-executable file at the receipt path.
It refuses a symbolic link, non-regular file, or non-executable file at the receipt-bound executable path.

The command also requires a trusted reachable route probe at `https://inference.local/v1/models` before it reports success.
When route repair needs the local Ollama dependency check, it verifies or recovers the authenticated proxy on port `11435` before it retries the route.
On every `ollama-local` completion path, the command verifies the authenticated proxy on port `11435`.
It also requires HTTP 2xx from `https://inference.local/v1/models` before it reports success.
If Ollama does not become healthy within 30 seconds, the command identifies the exact receipt-bound executable and its `serve` argument, then tells you to retry recovery.
An Ollama startup or route failure exits non-zero and prints the available recovery guidance.
</AgentOnly>
Expand Down
19 changes: 13 additions & 6 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1020,17 +1020,20 @@ Set `NEMOCLAW_NO_CONNECT_HINT=1` to suppress the hint in scripted workflows.
If the sandbox is running an outdated agent version, a non-blocking warning prints before connecting with a `$$nemoclaw <name> rebuild` hint.
If another terminal is already connected to the sandbox, `connect` prints a note with the number of existing sessions before proceeding. Multiple concurrent sessions are allowed.

`connect` does not pull a model itself, but it does inspect managed-vLLM install variables such as `NEMOCLAW_VLLM_MODEL` and `NEMOCLAW_VLLM_EXTRA_ARGS_JSON` if you exported them in the same shell.
Without `--probe-only`, `connect` does not pull a model itself, but it does inspect managed-vLLM install variables such as `NEMOCLAW_VLLM_MODEL` and `NEMOCLAW_VLLM_EXTRA_ARGS_JSON` if you exported them in the same shell.
An unknown model slug, malformed extra-args JSON, or a gated model (for example `deepseek-r1-distill-70b`) with no `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN` exits non-zero with the same error the installer would emit, before any sandbox readiness probe or SSH attach.
Unset the managed-vLLM variable, or fix the value, before retrying.
Unset the managed-vLLM variable, or fix the value, before retrying a regular connection.
`connect --probe-only` skips this install preflight so stale managed-vLLM variables cannot block recovery.

<AgentOnly variant="openclaw">
For a portable experimental-profile sandbox with the recorded `ollama-local` provider, `connect --probe-only` probes `http://127.0.0.1:11434/api/tags` before it decides whether to start Ollama.
If the API is unhealthy and the ownership receipt is valid, it starts the fixed user-local Ollama executable with `serve`.
An installation made before the ownership receipt was introduced is not claimed automatically.
After you verify that `${HOME}/.local/bin/ollama` is the executable you intend NemoClaw to manage, run `NEMOCLAW_PORTABLE_OLLAMA_REENROLL=1 nemoclaw <name> recover` once to record it explicitly.
The command rejects a symlink or non-executable file before it records ownership.
It refuses to launch a duplicate when another `ollama` process exists but the API remains unhealthy.
After a launch, it waits up to 30 seconds for valid `/api/tags` JSON with a `models` array.
It then requires the existing sandbox `inference.local` route probe to return a trusted reachable result before it exits successfully.
When route repair needs the local Ollama dependency check, it verifies or recovers the authenticated proxy before it retries `inference.local`.
It then verifies the authenticated proxy and requires HTTP 2xx from the sandbox `inference.local/v1/models` route before it exits successfully.
If Ollama does not become healthy within 30 seconds, the command identifies the exact receipt-bound executable and its `serve` argument, then tells you to retry recovery.
The command does not take over a system service or an unrelated user-managed Ollama daemon.
</AgentOnly>
Expand Down Expand Up @@ -1391,7 +1394,8 @@ $$nemoclaw my-assistant recover

<AgentOnly variant="openclaw">

For a portable experimental-profile sandbox with the recorded `ollama-local` provider, `recover` also runs the ownership-bound Ollama and `inference.local` route reachability checks described for [`connect --probe-only`](#$$nemoclaw-name-connect).
For a portable experimental-profile sandbox with the recorded `ollama-local` provider, `recover` also runs the ownership-bound Ollama checks described for [`connect --probe-only`](#$$nemoclaw-name-connect).
Before it reports success, every completion path verifies the authenticated proxy on port `11435` and requires HTTP 2xx from `inference.local/v1/models`.

</AgentOnly>

Expand Down Expand Up @@ -4496,7 +4500,10 @@ The following flags change defaults for commands that manage existing sandboxes.
| `NEMOCLAW_CONFIG_ACCEPT_NEW_PATH` | Exactly `"1"` to opt in (`true`, `yes`, `on` are not accepted) | Allows `$$nemoclaw <name> config set` to write a dotpath that does not already exist in the sandbox config, without the interactive confirmation. Equivalent to passing `--config-accept-new-path`, and it takes precedence over `NEMOCLAW_NON_INTERACTIVE=1`. Without it, a run without a TTY refuses the write instead. |
</AgentOnly>
| `NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE` | Exact JSON array of sandbox names | Confirms to the installer that the exact listed set of pre-fingerprint OpenClaw or Hermes sandboxes used NemoClaw-managed images, allowing recovery onto the current managed image. The normalized names must exactly match the installer's printed array. Set it only after verifying every named sandbox. Recorded custom-image evidence remains blocked. |
| `NEMOCLAW_DISABLE_INFERENCE_ROUTE_REPAIR` | `1` to enable | Skips the automatic DNS-proxy repair for stale `inference.local` routes during `$$nemoclaw <name> connect` and `$$nemoclaw <name> connect --probe-only`. Use only as a troubleshooting escape hatch. |
| `NEMOCLAW_DISABLE_INFERENCE_ROUTE_REPAIR` | `1` to enable | Skips automatic DNS-proxy mutation for stale `inference.local` routes during `$$nemoclaw <name> connect` and `$$nemoclaw <name> connect --probe-only`. The command still probes the route and exits non-zero when the provider-specific requirement fails. An `ollama-local` route still requires a healthy authenticated proxy and HTTP 2xx from `inference.local/v1/models`. Use only as a troubleshooting escape hatch. |
<AgentOnly variant="openclaw">
| `NEMOCLAW_PORTABLE_OLLAMA_REENROLL` | Exactly `1` for one recovery run | After you verify `${HOME}/.local/bin/ollama`, explicitly records that regular executable as NemoClaw-managed for a portable experimental-profile sandbox whose recorded provider is `ollama-local`. Use this once for a user-local Ollama that NemoClaw installed before ownership receipts existed. It never claims a system Ollama, symlink, non-executable file, or non-Ollama sandbox. |
</AgentOnly>
| `NEMOCLAW_DISABLE_SUPERVISOR_RELAUNCH` | `1` to enable | Skips the automatic trusted container recreation during `$$nemoclaw <name> recover` when two managed scans find no supervisor while PID 1 remains stable. Use only as a troubleshooting escape hatch; recovery then falls back to the rebuild or re-onboard guidance. |
| `NEMOCLAW_SHIELDS_ACCEPT_LEGACY_BASELINE` | `1` to opt in | Allows advanced immutable-config verification to trust the current on-disk bytes for older or partial content baselines. Use only after you have rebuilt or manually inspected the sandbox state and accepted that the baseline is operator-approved. |
| `NEMOCLAW_SHIELDS_SETTLE_MS` | milliseconds (default `750`, clamped to `0` to `10000`) | Settle window NemoClaw waits after re-applying a config lockdown (during shields auto-restore and `$$nemoclaw <name> shields up` drift remediation) before re-confirming the lock still holds. Detects when an in-sandbox reconciler changes config file permissions after lockdown and re-applies the lock; if NemoClaw cannot re-confirm the lock within the retry budget, shields stay down. This narrows the window in which a reconciler can revert permissions rather than eliminating it. The best-effort `chattr +i` immutable bit remains the only fully durable lock. Raise it on hosts where the gateway settles slowly. |
Expand Down
41 changes: 41 additions & 0 deletions src/lib/actions/sandbox/connect-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,47 @@ describe("connectSandbox flow", () => {
);
});

it.each([
401, 403, 404,
])("rejects HTTP %i from inference.local for an Ollama recovery path (#8502)", async (httpStatus) => {
const response = `OK ${String(httpStatus)}`;
const harness = createConnectHarness({
inferenceGetOutput: "Provider: ollama-local\nModel: qwen3-vl:4b\n",
inferenceProbeResponses: [response, response],
registryEntry: { provider: "ollama-local", model: "qwen3-vl:4b" },
});

await expect(harness.connectSandbox("alpha", { probeOnly: true })).rejects.toThrow(
"process.exit(1)",
);

expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain(
"inference.local/v1/models must return HTTP 2xx",
);
expect(harness.probeLocalProviderHealthSpy).toHaveBeenCalledWith("ollama-local", {
skipOllamaAuthProxySubprobe: true,
});
expect(harness.probeOllamaAuthProxyHealthSpy).toHaveBeenCalledTimes(1);
expect(exitSpy).toHaveBeenCalledWith(1);
});

it("rechecks HTTP 2xx after repairing an Ollama inference route (#8502)", async () => {
const harness = createConnectHarness({
inferenceGetOutput: "Provider: ollama-local\nModel: qwen3-vl:4b\n",
inferenceProbeResponses: ["BROKEN 503", "OK 401", "OK 401"],
registryEntry: { provider: "ollama-local", model: "qwen3-vl:4b" },
});

await expect(harness.connectSandbox("alpha", { probeOnly: true })).rejects.toThrow(
"process.exit(1)",
);

expect(harness.runSetupDnsProxySpy).toHaveBeenCalled();
expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain(
"inference.local/v1/models must return HTTP 2xx",
);
});

it("fails closed with actionable diagnostics when the initial route probe is inconclusive (#6192)", async () => {
const longProbeDetail = `route probe unavailable NVIDIA_API_KEY=super-secret ${"x".repeat(400)}`;
const harness = createConnectHarness({
Expand Down
11 changes: 6 additions & 5 deletions src/lib/actions/sandbox/connect-route-repair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,20 @@ function makeRepairDeps(
}

describe("sandbox connect route repair unit flow", () => {
it("skips work when route repair is disabled", () => {
const { calls, deps } = makeRepairDeps([], {
it("still probes and fails closed when route repair is disabled (#8502)", () => {
const { calls, deps } = makeRepairDeps([broken()], {
isRepairDisabled: () => true,
});

const result = repairSandboxInferenceRouteWithDeps("demo", sandbox(), {}, deps);

expect(result).toEqual({
healthy: true,
healthy: false,
repairAttempted: false,
detail: "route repair disabled",
detail: "route repair disabled; BROKEN 503",
});
expect(calls.probeOptions).toEqual([]);
expect(calls.probeOptions).toEqual([undefined]);
expect(calls.legacyRepairs).toEqual([]);
});

it("does not repair a healthy initial probe", () => {
Expand Down
43 changes: 36 additions & 7 deletions src/lib/actions/sandbox/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type SandboxListProbe = {
export type SandboxInferenceRouteProbe = {
healthy: boolean;
broken: boolean;
httpStatus?: number;
detail: string;
};

Expand Down Expand Up @@ -412,6 +413,7 @@ function probeSandboxInferenceRoute(
lastProbe = {
healthy: parsed.healthy,
broken: parsed.broken,
httpStatus: parsed.httpStatus,
detail: parsed.detail,
};
if (lastProbe.healthy || attempt === boundedAttempts) return lastProbe;
Expand Down Expand Up @@ -462,14 +464,18 @@ export function repairSandboxInferenceRouteWithDeps(
): SandboxInferenceRouteRepairResult {
const log = deps.log ?? console.log;
const error = deps.error ?? console.error;
if (deps.isRepairDisabled?.()) {
return { healthy: true, repairAttempted: false, detail: "route repair disabled" };
}
deps.assertRouteCompatible?.(sandboxName, sb);
const initialProbe = deps.probe(sandboxName);
if (initialProbe.healthy) {
return { healthy: true, repairAttempted: false, detail: initialProbe.detail };
}
if (deps.isRepairDisabled?.()) {
return {
healthy: false,
repairAttempted: false,
detail: `route repair disabled; ${initialProbe.detail}`,
};
}
if (!initialProbe.broken) {
return { healthy: false, repairAttempted: false, detail: initialProbe.detail };
}
Expand Down Expand Up @@ -809,14 +815,37 @@ function ensureSandboxInferenceRouteUnlocked(
}
return { sandbox: sb, routeHealthy: false };
}
if (!repairResult.healthy && repairResult.repairAttempted) {
const resetResult = resetManagedInferenceRoute(sandboxName, sb, agent, gatewayName, {
let routeReady = repairResult.healthy;
if (!routeReady && repairResult.repairAttempted) {
routeReady = resetManagedInferenceRoute(sandboxName, sb, agent, gatewayName, {
detail: repairResult.detail,
quiet,
});
return { sandbox: sb, routeHealthy: resetResult };
if (!routeReady) return { sandbox: sb, routeHealthy: false };
}
if (provider === "ollama-local") {
if (!verifyLocalInferenceRouteDependencies(provider, { quiet })) {
return { sandbox: sb, routeHealthy: false };
}
const finalProbe = probeSandboxInferenceRoute(sandboxName, agent);
const strictRouteHealthy =
finalProbe.healthy &&
finalProbe.httpStatus !== undefined &&
finalProbe.httpStatus >= 200 &&
finalProbe.httpStatus < 300;
if (!strictRouteHealthy) {
if (!quiet) {
printUnrecoverableInferenceRoute(
sandboxName,
`${sanitizeRouteValueForDisplay(provider)}/${sanitizeRouteValueForDisplay(model)}`,
`inference.local/v1/models must return HTTP 2xx; ${finalProbe.detail}`,
{ repairAttempted: repairResult.repairAttempted },
);
}
return { sandbox: sb, routeHealthy: false };
}
}
return { sandbox: sb, routeHealthy: repairResult.healthy };
return { sandbox: sb, routeHealthy: routeReady };
} catch (error) {
if (!sb || inference?.kind !== "configured") return { sandbox: sb, routeHealthy: null };
if (error instanceof OpenShellGatewayEndpointOverrideError) {
Expand Down
Loading
Loading