Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a29cebe
fix(onboard): preserve truthful review recovery
yimoj Aug 10, 2026
819c065
refactor(onboard): extract review recovery wiring
yimoj Aug 11, 2026
838f0e7
merge: update onboarding recovery branch
yimoj Aug 11, 2026
d4584db
test(onboard): satisfy review recovery budgets
yimoj Aug 11, 2026
52c7607
refactor(onboard): keep entrypoint within budget
yimoj Aug 11, 2026
343a0a8
test(onboard): cover review dependency ordering
yimoj Aug 11, 2026
bfadff9
refactor(onboard): reduce provider recovery complexity
yimoj Aug 11, 2026
4ead75b
chore(onboard): apply repository formatting
yimoj Aug 11, 2026
62adee2
merge: refresh onboarding recovery base
yimoj Aug 11, 2026
dbd9c2f
test(onboard): keep review fixture branchless
yimoj Aug 11, 2026
592139d
Merge branch 'main' into fix/8686-onboard-cancellation-status
cv Aug 11, 2026
fe75ba2
Merge branch 'main' into fix/8686-onboard-cancellation-status
cv Aug 11, 2026
ddcef29
Merge branch 'main' into fix/8686-onboard-cancellation-status
cv Aug 11, 2026
5c856c5
test(onboard): cover proxy preparation failures
apurvvkumaria Aug 11, 2026
35ac5d3
merge: refresh onboarding recovery base
apurvvkumaria Aug 11, 2026
68dd02a
merge: incorporate onboarding recovery branch updates
apurvvkumaria Aug 11, 2026
ed7271d
fix(onboard): defer Ollama proxy preparation
apurvvkumaria Aug 11, 2026
acfc89b
Merge branch 'main' into fix/8686-onboard-cancellation-status
cv Aug 11, 2026
9fd77cd
docs(onboard): explain configuration review recovery
apurvvkumaria Aug 11, 2026
85b6ac5
merge(main): refresh PR #8724
apurvvkumaria Aug 11, 2026
41dda5c
test(onboard): align Ollama autostart sentinel
apurvvkumaria Aug 11, 2026
4ca80d2
Merge branch 'main' into fix/8686-onboard-cancellation-status
cv Aug 11, 2026
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
7 changes: 5 additions & 2 deletions docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,11 @@ Use these details when your first-run path needs more control.
```

The default is `Y`.
Press Enter to continue, or answer `n` to abort cleanly, correct the entries, and rerun `nemoclaw onboard`.
Non-interactive runs print the summary for log clarity but skip the prompt.
Press Enter to accept the configuration.
If you answer `n`, onboarding exits with a nonzero status and clears the recorded provider, model, and sandbox name.
The rejected run does not register a new gateway credential.
Run `nemoclaw onboard` to make new choices.
Non-interactive runs print the summary and skip the prompt.
</Accordion>

<Accordion title="Web Search Messaging and Network Policies">
Expand Down
2 changes: 1 addition & 1 deletion docs/inference/set-up-ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ The agent inside the sandbox never receives the token directly because the OpenS
The wizard manages the proxy lifecycle:

- It generates a random 24-byte token and stores it in `~/.nemoclaw/ollama-proxy-token` with `0600` permissions.
- It starts and verifies the proxy after Ollama.
- It starts and verifies the proxy only after you accept the onboarding configuration.
- It removes stale matching proxy processes from previous runs.
- It probes the sandbox Docker network path before saving the inference route.
- It stops matching proxy processes during uninstall.
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,16 @@ For a profile-backed session, resume requires the same catalog, preset, and reci
Omit `--profile` to reuse that recorded selection, or pass the same profile explicitly; use `--fresh` to adopt a changed catalog definition.
Legacy sessions without a profile-provenance record continue to resume normally, but cannot acquire a new `--profile` selection during resume.

Before the configuration review, NemoClaw records the sandbox name and the selected provider and model as an incomplete choice.
If onboarding stops at the review prompt, an interactive `--resume` run shows the prompt again.
A non-interactive `--resume` run reuses the recorded choice and continues to inference setup.
After you accept the review, NemoClaw records the choice before inference setup starts.
If inference setup fails, `--resume` reuses the accepted provider, model, and sandbox name.
If you reject the review, onboarding exits with a nonzero status and clears those recorded choices.
Run `$$nemoclaw onboard` to make new choices after rejection.
During a resume without terminal input, `--yes` or `NEMOCLAW_YES=1` also selects non-interactive resume behavior.
For a new or fresh session, `--yes` and `NEMOCLAW_YES=1` accept supported confirmations but do not replace `--non-interactive`.

<AgentOnly variant="openclaw">

OpenClaw sessions also record the web search selection, messaging selection and non-secret settings, and resource profile.
Expand Down
3 changes: 2 additions & 1 deletion src/lib/core/non-interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ afterEach(() => {
});

describe("non-interactive environment detection", () => {
it("treats only the canonical value as non-interactive", () => {
it("treats only the canonical explicit value as non-interactive", () => {
expect(isNonInteractiveEnv({ NEMOCLAW_NON_INTERACTIVE: "1" } as NodeJS.ProcessEnv)).toBe(true);
expect(isNonInteractiveEnv({ NEMOCLAW_NON_INTERACTIVE: "true" } as NodeJS.ProcessEnv)).toBe(
false,
);
expect(isNonInteractiveEnv({ NEMOCLAW_NON_INTERACTIVE: "" } as NodeJS.ProcessEnv)).toBe(false);
expect(isNonInteractiveEnv({ NEMOCLAW_YES: "1" } as NodeJS.ProcessEnv)).toBe(false);
expect(isNonInteractiveEnv({} as NodeJS.ProcessEnv)).toBe(false);
});

Expand Down
32 changes: 10 additions & 22 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,7 +1061,6 @@ const {
ensureOllamaLoopbackSystemdOverride,
runOllamaStartupOrGate,
shouldFrontOllamaWithProxy,
startOllamaAuthProxy,
getLocalProviderBaseUrl,
selectAndValidateOllamaModel,
printOllamaExposureWarning,
Expand Down Expand Up @@ -3671,6 +3670,7 @@ const {

const startRecordedStep = onboardRuntimeBoundary.startRecordedStep.bind(onboardRuntimeBoundary);
const recordStepComplete = onboardRuntimeBoundary.recordStepComplete.bind(onboardRuntimeBoundary);
const recordStepRejected = onboardRuntimeBoundary.recordStepRejected.bind(onboardRuntimeBoundary);
const recordStepSkipped = onboardRuntimeBoundary.recordStepSkipped.bind(onboardRuntimeBoundary);
const recordStepFailed = onboardRuntimeBoundary.recordStepFailed.bind(onboardRuntimeBoundary);
const recordStateSkipped = onboardRuntimeBoundary.recordStateSkipped.bind(onboardRuntimeBoundary);
Expand Down Expand Up @@ -3752,33 +3752,16 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
authoritativeGateway?.name ?? GATEWAY_NAME,
);
setOnboardBrandingAgent(opts.agent || process.env.NEMOCLAW_AGENT || null);
NON_INTERACTIVE = opts.nonInteractive || isNonInteractiveEnv();
RECREATE_SANDBOX = opts.recreateSandbox || process.env.NEMOCLAW_RECREATE_SANDBOX === "1";
AUTO_YES = opts.autoYes === true || process.env.NEMOCLAW_YES === "1";
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const { fresh, nonInteractive, requestedFromDockerfile, requestedSandboxName, cannotPrompt, resume } = onboardEntryOptions.resolveOnboardRunEntryOptions(opts, process.env, onboardSession.loadSession()?.status ?? null, isNonInteractiveEnv, { validateName, reservedSandboxNames: RESERVED_SANDBOX_NAMES, cliDisplayName, getNameValidationGuidance, error: (message) => console.error(message), exitProcess: (code) => process.exit(code) });
NON_INTERACTIVE = nonInteractive;
RECREATE_SANDBOX = opts.recreateSandbox || process.env.NEMOCLAW_RECREATE_SANDBOX === "1";
_preflightDashboardPort =
opts.controlUiPort ?? (process.env.NEMOCLAW_DASHBOARD_PORT != null ? DASHBOARD_PORT : null);
onboardRuntimeBoundary.reset();
if (!authoritativeGateway) delete process.env.OPENSHELL_GATEWAY;
preparedDcodeRuntime.applyGatewayEnv(process.env);
const { resume, fresh, requestedFromDockerfile, requestedSandboxName, cannotPrompt } =
onboardEntryOptions.resolveOnboardEntryOptions(
{
opts,
env: process.env,
stdinIsTty: Boolean(process.stdin && process.stdin.isTTY),
stdoutIsTty: Boolean(process.stdout && process.stdout.isTTY),
persistedSessionStatus: onboardSession.loadSession()?.status ?? null,
},
{
isNonInteractive,
validateName,
reservedSandboxNames: RESERVED_SANDBOX_NAMES,
cliDisplayName,
getNameValidationGuidance,
error: (message) => console.error(message),
exitProcess: (code) => process.exit(code),
},
);
const baseImageResolutionContext = baseImageResolutionFlow.createBaseImageResolutionContext({
fresh,
initialHint: opts.baseImageResolutionHint,
Expand Down Expand Up @@ -4111,6 +4094,9 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const endpointProvenance = { endpointSource: opts.endpointSource, endpointSourceProvider: opts.rebuildRegistryInferenceRoute?.route.provider ?? null, endpointSourceEndpointUrl: opts.rebuildRegistryInferenceRoute?.route.endpointUrl ?? null, getSandboxRegistryEntry: registry.getSandbox };
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const providerReviewDeps = setupInferenceFactory.createProviderReviewDeps(onboardSession.updateSession, onboardSessionBootstrap.checkpointSandboxName, { shouldFrontOllamaWithProxy, startOllamaAuthProxy, getOllamaProxyToken, persistAndProbeOllamaProxy }, process.exit, console.error);
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const coreFlowPhases = createCoreOnboardFlowPhases<InitialOnboardFlowContext, unknown, MessagingChannelConfig, import("./resources-cmd").ResourceProfile>({
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
resumeProvider: { isNonInteractive, isRoutedInferenceProvider, providerExistsInGateway, replaceNamedCredential, resumeManagedLlamaCppRuntime: (sandboxName) => setupNimFlow.resumeManagedLlamaCppRuntime(sandboxName, { gatewayPort: GATEWAY_PORT, runtimeProvider: setupNimFlow.resolveCurrentRuntimeProviderBundle() }) },
Expand All @@ -4136,6 +4122,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
setupInference,
startRecordedStep,
recordStepComplete,
recordStepRejected,
toSessionUpdates: (updates) =>
toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
skippedStepMessage,
Expand Down Expand Up @@ -4167,6 +4154,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
},
reserveSandboxInferenceRoute: registry.reserveSandboxInferenceRoute,
registryUpdateSandbox: (name, updates) => registry.updateSandbox(name, updates),
...providerReviewDeps,
promptValidatedSandboxName,
assessHost,
formatSandboxBuildEstimateNote,
Expand Down
53 changes: 53 additions & 0 deletions src/lib/onboard/entry-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { describe, expect, it, vi } from "vitest";
import {
type OnboardEntryOptionsDeps,
resolveOnboardEntryOptions,
resolveOnboardRunOptions,
withNonInteractiveEnvironment,
} from "./entry-options";

Expand All @@ -30,6 +31,41 @@ function createDeps(overrides: Partial<OnboardEntryOptionsDeps> = {}): OnboardEn
};
}

describe("resolveOnboardRunOptions", () => {
it.each([
[false, true],
[false, false],
])("treats auto-yes resume as non-interactive when stdin=%s and stdout=%s", (stdinIsTty, stdoutIsTty) => {
expect(
resolveOnboardRunOptions({ autoYes: true, resume: true }, {}, null, () => false, {
stdinIsTty,
stdoutIsTty,
}).nonInteractive,
).toBe(true);
});

it.each([
[true, true],
[true, false],
])("keeps auto-yes resume interactive when stdin=%s and stdout=%s", (stdinIsTty, stdoutIsTty) => {
expect(
resolveOnboardRunOptions({ autoYes: true, resume: true }, {}, null, () => false, {
stdinIsTty,
stdoutIsTty,
}).nonInteractive,
).toBe(false);
});

it("keeps fresh no-TTY auto-yes interactive", () => {
expect(
resolveOnboardRunOptions({ autoYes: true }, {}, null, () => false, {
stdinIsTty: false,
stdoutIsTty: false,
}).nonInteractive,
).toBe(false);
});
});

describe("resolveOnboardEntryOptions", () => {
it("rejects mutually exclusive resume and fresh flags", () => {
const deps = createDeps();
Expand Down Expand Up @@ -157,6 +193,23 @@ describe("resolveOnboardEntryOptions", () => {
expect(deps.error).not.toHaveBeenCalled();
});

it("does not auto-resume a rejected non-resumable session", () => {
const deps = createDeps();

const result = resolveOnboardEntryOptions(
{
opts: {},
env: {},
stdinIsTty: true,
stdoutIsTty: true,
persistedSessionStatus: "failed",
},
deps,
);

expect(result.resume).toBe(false);
});

it("does not auto-resume when --fresh is set even with an in_progress session (#5470)", () => {
const deps = createDeps();

Expand Down
45 changes: 45 additions & 0 deletions src/lib/onboard/entry-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,51 @@ export interface ResolvedOnboardEntryOptions {

type NonInteractiveEntryOptions = { nonInteractive?: boolean };
type ResumableEntryOptions = NonInteractiveEntryOptions & { resume?: boolean; fresh?: boolean };

export function resolveOnboardRunOptions(
options: OnboardEntryOptionsInput["opts"] & { autoYes?: boolean; nonInteractive?: boolean },
env: NodeJS.ProcessEnv,
persistedSessionStatus: string | null,
isNonInteractiveEnv: () => boolean,
terminal: { stdinIsTty: boolean; stdoutIsTty: boolean } = {
stdinIsTty: Boolean(process.stdin?.isTTY),
stdoutIsTty: Boolean(process.stdout?.isTTY),
},
) {
const resume =
options.resume === true || (options.fresh !== true && persistedSessionStatus === "in_progress");
const nonInteractive =
options.nonInteractive === true ||
((options.autoYes === true || env.NEMOCLAW_YES === "1") && resume && !terminal.stdinIsTty) ||
isNonInteractiveEnv();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return {
resume,
nonInteractive,
entryOptionsInput: { opts: options, env, ...terminal, persistedSessionStatus },
};
}

export function resolveOnboardRunEntryOptions(
options: OnboardEntryOptionsInput["opts"] & { autoYes?: boolean; nonInteractive?: boolean },
env: NodeJS.ProcessEnv,
persistedSessionStatus: string | null,
isNonInteractiveEnv: () => boolean,
deps: Omit<OnboardEntryOptionsDeps, "isNonInteractive">,
) {
const context = resolveOnboardRunOptions(
options,
env,
persistedSessionStatus,
isNonInteractiveEnv,
);
return {
...context,
...resolveOnboardEntryOptions(context.entryOptionsInput, {
...deps,
isNonInteractive: () => context.nonInteractive,
}),
};
}
interface StationExpressSessionLifecycle {
loadSession(): StationExpressSessionLike | null;
reconcileStationExpressReceiptRetirement(generation: string): void;
Expand Down
23 changes: 15 additions & 8 deletions src/lib/onboard/inference-providers/ollama-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
import type { OllamaDeps, SetupInferenceResult } from "./types";

export async function setupOllamaLocalInference(
args: { model: string; provider: string; allowToolsIncompatible: boolean },
args: {
model: string;
provider: string;
allowToolsIncompatible: boolean;
preparedProxyToken?: string;
},
deps: OllamaDeps,
): Promise<{ done: true; result: SetupInferenceResult } | { done: false }> {
const { model, provider, allowToolsIncompatible } = args;
Expand Down Expand Up @@ -40,7 +45,7 @@ export async function setupOllamaLocalInference(
// Try to start/restart the auth proxy before probing — this recovers
// from stale or missing proxy processes before we decide to abort.
if (frontOllamaWithProxy) {
ensureOllamaAuthProxy();
if (!args.preparedProxyToken) ensureOllamaAuthProxy();
proxyReady = isProxyHealthy();
}
if (proxyReady) {
Expand All @@ -66,17 +71,19 @@ export async function setupOllamaLocalInference(
const baseUrl = getLocalProviderBaseUrl(provider);
let ollamaCredential = "ollama";
if (frontOllamaWithProxy) {
// Skip if already started during the fallback recovery above.
if (!proxyReady) ensureOllamaAuthProxy();
const proxyToken = getOllamaProxyToken();
// The normal onboarding path prepares the proxy once, after review. The
// fallback remains for recovery callers that enter provider setup without
// a prepared token.
if (!args.preparedProxyToken && !proxyReady) ensureOllamaAuthProxy();
const proxyToken = args.preparedProxyToken ?? getOllamaProxyToken();
if (!proxyToken) {
error(" Ollama auth proxy token is not set. Re-run onboard to initialize the proxy.");
return exitProcess(1);
}
ollamaCredential = proxyToken;
// Persist token now that ollama-local is confirmed as the provider.
// Not persisted earlier in case the user backs out to a different provider.
await persistAndProbeOllamaProxy(proxyToken);
if (!args.preparedProxyToken) {
await persistAndProbeOllamaProxy(proxyToken);
}
}
// Use a dedicated internal credential env (NEMOCLAW_OLLAMA_PROXY_TOKEN)
// so the gateway never reads the user's host OPENAI_API_KEY for local
Expand Down
3 changes: 3 additions & 0 deletions src/lib/onboard/machine/core-flow-phases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ function createPhases(
recordStepComplete: vi.fn(async (_stepName: string, updates: SessionUpdates = {}) =>
sessionWithUpdates(updates),
),
recordStepRejected: vi.fn(async () => createSession()),
toSessionUpdates: (updates) => updates as SessionUpdates,
skippedStepMessage: vi.fn(),
ensureManagedLlamaCppResumeReady: vi.fn(async () => false),
Expand Down Expand Up @@ -183,6 +184,8 @@ function createPhases(
}),
reserveSandboxInferenceRoute: vi.fn(() => true),
registryUpdateSandbox: vi.fn(),
checkpointSandboxIdentity: vi.fn(async () => undefined),
prepareLocalProviderForInference: vi.fn(async () => null),
promptValidatedSandboxName: vi.fn(async () => "my-sandbox"),
assessHost: () => ({ memoryGb: 64 }),
formatSandboxBuildEstimateNote: () => null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function createDeps() {
setupInference: calls.setupInference,
startRecordedStep: vi.fn(async () => undefined),
recordStepComplete: calls.recordStepComplete,
recordStepRejected: vi.fn(async () => createSession()),
toSessionUpdates: (updates: Record<string, unknown>) => updates as SessionUpdates,
skippedStepMessage: vi.fn(),
ensureManagedLlamaCppResumeReady: vi.fn(async () => false),
Expand All @@ -118,6 +119,8 @@ function createDeps() {
reupsertRoutedProvider: calls.reupsertRoutedProvider,
reserveSandboxInferenceRoute: calls.reserveRoute,
registryUpdateSandbox: calls.updateSandbox,
checkpointSandboxIdentity: vi.fn(async () => undefined),
prepareLocalProviderForInference: vi.fn(async () => null),
promptValidatedSandboxName: vi.fn(async () => "target-sandbox"),
assessHost: () => ({ cpus: 8 }),
formatSandboxBuildEstimateNote: () => "estimate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function createDeps(
setupInference: vi.fn(async () => ({ ok: true as const })),
startStep: vi.fn(async () => undefined),
complete: vi.fn(async () => createSession()),
rejected: vi.fn(async () => createSession()),
skipped: vi.fn(),
recoverProvider: vi.fn(
async (
Expand Down Expand Up @@ -133,6 +134,8 @@ export function createDeps(
),
reserveRoute: vi.fn(() => true),
updateSandbox: vi.fn(),
checkpointSandboxIdentity: vi.fn(async () => undefined),
prepareLocalProviderForInference: vi.fn(async () => null),
promptName: vi.fn(async () => "my-assistant"),
promptYesNo: vi.fn(async () => true),
log: vi.fn(),
Expand All @@ -158,6 +161,7 @@ export function createDeps(
setupInference: calls.setupInference,
startRecordedStep: calls.startStep,
recordStepComplete: calls.complete,
recordStepRejected: calls.rejected,
toSessionUpdates: (updates: Record<string, unknown>) => updates as SessionUpdates,
skippedStepMessage: calls.skipped,
ensureManagedLlamaCppResumeReady: calls.recoverManagedLlamaCpp,
Expand Down Expand Up @@ -195,6 +199,8 @@ export function createDeps(
reupsertRoutedProvider: calls.reupsertRoutedProvider,
reserveSandboxInferenceRoute: calls.reserveRoute,
registryUpdateSandbox: calls.updateSandbox,
checkpointSandboxIdentity: calls.checkpointSandboxIdentity,
prepareLocalProviderForInference: calls.prepareLocalProviderForInference,
promptValidatedSandboxName: calls.promptName,
assessHost: () => ({ cpus: 8 }),
formatSandboxBuildEstimateNote: () => "estimate",
Expand Down
Loading
Loading