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
12 changes: 12 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,18 @@ For example, `nemohermes` resolves to `hermes`, while `dcode`, `deepagents`, `de

NemoClaw records onboarding progress so interrupted runs can continue.
Use `--resume` to continue a resumable onboarding session with the provider, model, sandbox name, agent, observability choice, and custom Dockerfile path recorded by the original run.

<AgentOnly variant="openclaw">

OpenClaw sessions also record the web search selection, messaging selection and non-secret settings, and resource profile.
When the saved session includes prompt checkpoints, resume skips each completed group and continues at the first incomplete choice.
Legacy sessions without those checkpoints may repeat choices whose completion cannot be proven.
Raw web search and messaging credentials are never written to the onboarding session.
Resume skips their secret prompts when the same session recorded a successful OpenShell provider registration and OpenShell still reports the exact expected name, type, and credential keys.
If the session lacks that registration receipt, the provider is missing, or its binding does not match, interactive resume requests the credential again; non-interactive resume preserves the completed choice, reports the required environment variable, and exits so you can export it before retrying.

</AgentOnly>

Completed onboarding sessions are not resumable.
Use `--resume` only for interrupted `in_progress` sessions, not to change provider, model, agent, or sandbox recreation settings after onboarding has completed.
During resume, NemoClaw reruns preflight, gateway, provider, and sandbox repair checks even when the saved session has already reached a later nonterminal onboarding phase.
Expand Down
11 changes: 10 additions & 1 deletion docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -774,13 +774,22 @@ Silent retry would loop on the same failure if your original choice, such as an
curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_FRESH=1 bash
```

Retry the same session without re-prompting.
Retry the same session.

This is only useful if the original failure was transient, for example a network blip or a stopped Docker daemon, and not a wrong provider choice:

```bash
$$nemoclaw onboard --resume
```

<AgentOnly variant="openclaw">

OpenClaw resume does not repeat completed non-secret sandbox, web search, messaging, or resource choices.
Resume also reuses registered web search and messaging credentials when the same onboarding session recorded their successful OpenShell registration and OpenShell still reports the exact expected name, type, and credential keys.
If the session lacks that registration receipt, the provider is missing, or its binding does not match, interactive resume requests the credential again; non-interactive resume preserves the completed choice, reports the required environment variable, and exits so you can export it before retrying `$$nemoclaw onboard --resume`.

</AgentOnly>

As a last resort, you can also delete the session file directly and re-run the installer:

```bash
Expand Down
13 changes: 13 additions & 0 deletions docs/security/credential-storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ For any other missing-provider case, rerun `$$nemoclaw onboard` or re-register t
## Onboarding Reads Credentials from Environment

`$$nemoclaw onboard` reads credentials from the host environment, registers them with the OpenShell gateway, and creates the sandbox.

<AgentOnly variant="openclaw">

After the sandbox name and web search choices are checkpointed, NemoClaw registers a selected, validated web search credential before messaging setup.
After messaging choices are checkpointed, it registers selected, validated messaging credentials before resource selection.
It creates providers with the expected name, type, and credential key, and updates an existing provider only when that binding matches exactly.
If onboarding is interrupted afterward, `--resume` reuses a provider only when the same session recorded that successful registration and the live binding still matches the saved choice.
The session stores only those non-secret provider names; raw values never enter `~/.nemoclaw/onboard-session.json`.
Because registration precedes sandbox creation, an abandoned run can leave a provider behind.
Retry onboarding with the same sandbox name to reconcile that provider.

</AgentOnly>

A typical onboarding invocation looks like:

```bash
Expand Down
79 changes: 39 additions & 40 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ const inferenceConfig: typeof import("./inference/config") = require("./inferenc
const { DEFAULT_CLOUD_MODEL, getProviderSelectionConfig, parseGatewayInference } = inferenceConfig;

const onboardProviders = require("./onboard/providers");
const credentialProviderRegistration: typeof import("./onboard/credential-provider-registration") = require("./onboard/credential-provider-registration");
const inferenceProviders: typeof import("./onboard/inference-providers") = require("./onboard/inference-providers");
const setupInferenceFactory: typeof import("./onboard/setup-inference") =
require("./onboard/setup-inference");
Expand Down Expand Up @@ -448,6 +449,8 @@ const promptValidatedSandboxName = sandboxAgent.createPromptValidatedSandboxName
promptOrDefault,
cliDisplayName,
isNonInteractive,
checkpointSandboxName: (sandboxName, agent) =>
onboardSessionBootstrap.checkpointSandboxName(sandboxName, agent, onboardSession.updateSession),
exit: process.exit,
});
const modelRouter: typeof import("./onboard/model-router") = require("./onboard/model-router");
Expand Down Expand Up @@ -967,37 +970,20 @@ const verifyDirectSandboxGpu = sandboxGpuPreflight.createDirectSandboxGpuVerifie
redact,
});

function upsertMessagingProviders(
tokenDefs: MessagingTokenDef[],
options: { replaceExisting?: boolean } = {},
) {
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
braveProviderProfile.ensureWebSearchProviderProfiles(tokenDefs, { root: ROOT, runOpenshell, redact });
const upserted = onboardProviders.upsertMessagingProviders(tokenDefs, runOpenshell, options);
// upsertMessagingProviders process.exits on failure, so reaching this
// point means every entry in tokenDefs that had a token was registered.
// Mark migrated only when the registered token equals the staged legacy
// value — a token rotated since staging (or a fresh prompt) is not a
// legacy migration even if it happens to use the same env-key name.
// Mirror upsertProvider's withdrawal logic so a later messaging upsert
// that replaces the legacy value with something else cannot leave the
// mark stuck on.
let mutated = false;
for (const def of tokenDefs) {
if (!def.token || !def.envKey) continue;
const stagedValue = stagedLegacyValues.get(def.envKey);
if (stagedValue === undefined) continue;
if (def.token === stagedValue) {
migratedLegacyKeys.add(def.envKey);
mutated = true;
} else {
migratedLegacyKeys.delete(def.envKey);
mutated = true;
}
}
if (mutated) persistMigratedLegacyKeys();
return upserted;
}
const registeredCredentialProviders =
credentialProviderRegistration.createCredentialProviderRegistration({
root: ROOT,
runOpenshell,
redact,
getGatewayName: () => GATEWAY_NAME,
normalizeCredentialValue,
updateSession: onboardSession.updateSession,
stagedLegacyValues,
migratedLegacyKeys,
persistMigratedLegacyKeys,
});
const { upsertMessagingProviders, providerMatchesGatewayCredential } =
registeredCredentialProviders;
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const providerExistsInGateway = (name: string, gatewayName: string = GATEWAY_NAME) => onboardProviders.providerExistsInGateway(name, setupInferenceFactory.createGatewayScopedOpenshellRunner(runOpenshell, gatewayName));

Expand Down Expand Up @@ -2309,6 +2295,7 @@ async function createSandboxWithBaseImageResolution(
hermesToolGateways,
extraProviders: extraProviderPlan.extraProviders,
staleExtraProviders: extraProviderPlan.staleExtraProviders,
...(createIntent?.reuseRegisteredCredentials ? { reuseRegisteredCredentials: true } : {}),
...(createIntent?.policyTier !== undefined ? { policyTier: createIntent.policyTier } : {}),
}));
const messagingCapabilities = await sandboxCreateIntentResolver.rebind(
Expand All @@ -2317,6 +2304,7 @@ async function createSandboxWithBaseImageResolution(
enabledChannels,
webSearchConfig,
agent,
...(createIntent?.reuseRegisteredCredentials ? { reuseRegisteredCredentials: true } : {}),
},
resolvedCreateIntent,
);
Expand Down Expand Up @@ -2707,6 +2695,7 @@ async function createSandboxWithBaseImageResolution(
enabledChannels,
webSearchConfig,
agent,
...(createIntent?.reuseRegisteredCredentials ? { reuseRegisteredCredentials: true } : {}),
},
resolvedCreateIntent,
);
Expand Down Expand Up @@ -3745,7 +3734,7 @@ const sandboxCreateIntentResolver = sandboxCreateIntentResolution.createSandboxC
>({
channels: MESSAGING_CHANNELS,
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
messagingPreflightDeps: { readMessagingPlanFromEnv, resolveDisabledChannels: channelState.resolveDisabledChannels, gatewayName: () => GATEWAY_NAME, registry, providerExistsInGateway, isNonInteractive, promptYesNoOrDefault, cliName, log: (message) => console.log(message), error: (message) => console.error(message), exitProcess: (code) => process.exit(code), getValidatedMessagingTokenByEnvKey, getCredential, normalizeCredentialValue, registerExtraPlaceholderProviders: extraPlaceholderKeysModule.registerExtraPlaceholderProviders, getMessagingChannelForEnvKey },
messagingPreflightDeps: { readMessagingPlanFromEnv, resolveDisabledChannels: channelState.resolveDisabledChannels, gatewayName: () => GATEWAY_NAME, registry, providerExistsInGateway, providerMatchesGatewayCredential, isNonInteractive, promptYesNoOrDefault, cliName, log: (message) => console.log(message), error: (message) => console.error(message), exitProcess: (code) => process.exit(code), getValidatedMessagingTokenByEnvKey, getCredential, normalizeCredentialValue, registerExtraPlaceholderProviders: extraPlaceholderKeysModule.registerExtraPlaceholderProviders, getMessagingChannelForEnvKey },
filterEnabledChannelsByAgent,
defaultPolicyPath: path.join(ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"),
getAgentPolicyPath: (agent) => (agent ? agentOnboard.getAgentPolicyPath(agent) : null),
Expand All @@ -3762,6 +3751,9 @@ const sandboxCreateIntentResolver = sandboxCreateIntentResolution.createSandboxC
}),
});

// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const stageSandboxCredentialProviders = (input: import("./onboard/credential-provider-registration").StageSandboxCredentialProvidersInput<AgentDefinition | null>) => registeredCredentialProviders.stageSandboxCredentialProviders(input, sandboxCreateIntentResolver.prepareCredentialProviders);

function getRecordedMessagingChannelsForResume(
resume: boolean,
session: Session | null,
Expand All @@ -3782,12 +3774,14 @@ async function setupMessagingChannels(
agent: AgentDefinition | null = null,
existingChannels: string[] | null = null,
sandboxName: string | null = null,
options: { readonly selectionCompleted?: boolean } = {},
): Promise<string[]> {
return setupMessagingChannelsImpl(agent, existingChannels, {
step,
note,
isNonInteractive,
sandboxName,
selectionCompleted: options.selectionCompleted,
});
}

Expand Down Expand Up @@ -4217,7 +4211,11 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {

const recordedSandboxName =
session?.steps?.sandbox?.status === "complete" ? session?.sandboxName || null : null;
const gatewaySandboxName = resume ? (recordedSandboxName ?? requestedSandboxName) : null;
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const checkpointedSandboxName = onboardSessionBootstrap.getCheckpointedSandboxName(resume, agent, session);
const gatewaySandboxName = resume
? (recordedSandboxName ?? requestedSandboxName ?? checkpointedSandboxName)
: null;
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const onboardGateway = gatewayBinding.resolveCoreOnboardGatewayBinding({ authoritativeGateway, currentGateway: { name: GATEWAY_NAME, port: GATEWAY_PORT }, resume, sandbox: gatewaySandboxName ? registry.getSandbox(gatewaySandboxName) : null });
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
Expand All @@ -4243,7 +4241,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
agent,
recordedSandboxName,
requestedSandboxName,
sandboxName: recordedSandboxName || requestedSandboxName || null,
sandboxName: recordedSandboxName || requestedSandboxName || checkpointedSandboxName || null,
fromDockerfile,
model: session?.model || null,
provider: session?.provider || null,
Expand Down Expand Up @@ -4347,12 +4345,10 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
const { gpuPassthrough } = initialContext;
const gpu = initialContext.gpu ?? null;

// #2753: prefer requestedSandboxName over an unconfirmed session name.
// A pre-fix session may carry sandboxName even though sandbox creation
// never completed; users supplying `--name` / NEMOCLAW_SANDBOX_NAME on
// the resume run must win, otherwise the stale name silently overrides
// their explicit recovery input.
let sandboxName = recordedSandboxName || requestedSandboxName || null;
// #2753: for an unfinished sandbox, an explicit requested name precedes
// the checkpointed name from the interrupted session.
let sandboxName =
recordedSandboxName || requestedSandboxName || checkpointedSandboxName || null;
if (sandboxName && RESERVED_SANDBOX_NAMES.has(sandboxName)) {
console.error(
` Reserved name in resumed session: '${sandboxName}' is a ${cliDisplayName()} CLI command.`,
Expand Down Expand Up @@ -4481,11 +4477,14 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
configureWebSearch,
startRecordedStep,
getRecordedMessagingChannelsForResume,
showMessagingStage: () => step(5, 8, "Messaging channels"),
setupMessagingChannels,
readMessagingPlanFromEnv,
writePlanToEnv,
clearPlanEnv,
getRegistrySandboxMessagingPlan,
providerMatchesGatewayCredential,
stageSandboxCredentialProviders,
promptValidatedSandboxName,
selectResourceProfileForSandbox: () =>
selectResourceProfileForSandbox({ isNonInteractive, note, prompt, promptOrDefault }),
Expand Down
6 changes: 6 additions & 0 deletions src/lib/onboard/agent-resume-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export function clearAgentScopedResumeState(session: Session, selectedAgentName:
session.nimContainer = null;
session.routerPid = null;
session.routerCredentialHash = null;
session.webSearchConfig = null;
session.messagingPlan = null;
if (session.sandboxPromptProgress) {
session.sandboxPromptProgress.webSearch = false;
session.sandboxPromptProgress.messaging = false;
}
session.policyPresets = null;

const resetSteps = [
Expand Down
Loading
Loading