Skip to content

refactor(onboard): defer credential persistence until the review gate confirms#2226

Closed
latenighthackathon wants to merge 2 commits into
NVIDIA:mainfrom
latenighthackathon:refactor/defer-credential-persist
Closed

refactor(onboard): defer credential persistence until the review gate confirms#2226
latenighthackathon wants to merge 2 commits into
NVIDIA:mainfrom
latenighthackathon:refactor/defer-credential-persist

Conversation

@latenighthackathon

@latenighthackathon latenighthackathon commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #2221 addressing the remaining credential-persistence window CodeRabbit flagged.

`setupNim`'s credential prompt writes the API key to `~/.nemoclaw/credentials.json` via `saveCredential` as soon as the user types a valid value. An abort at the review gate further down still left that key on disk — the existing abort message worked around it by directing users to `nemoclaw credentials reset `.

This PR introduces a module-level defer flag so credential-prompt writes are held in memory (`process.env` only) and only flushed to disk after the review gate confirms. A clean abort now leaves nothing on disk.

Stacks on #2221

This PR depends on #2221 landing first — it wires the new `flushDeferredCredentials` / `discardDeferredCredentials` calls into the review gate that #2221 adds.

Mechanism

  • `beginDeferredCredentialPersistence()` — arm the flag and clear the pending set. Called once at the top of the inference retry loop in `onboard()`, only when the review gate will actually fire (interactive + not `--dangerously-skip-permissions`).
  • `replaceNamedCredential()` — when the flag is armed, set `process.env` and add the env name to `pendingDeferredCredentials`; skip the `saveCredential` call. Prints `Key held in memory; will be saved to credentials.json on confirm.` instead of the usual `Key saved`.
  • `flushDeferredCredentials()` — iterate the pending set, call `saveCredential(envName, process.env[envName])` for each, clear the set. Flag stays armed so retry-loop iterations after an inference validation failure continue to defer their replacement keys.
  • `discardDeferredCredentials()` — clear the set and disarm the flag. Called on gate abort — nothing ever reached disk.
  • `endDeferredCredentialPersistence()` — clear + disarm. Called after the retry loop breaks successfully so downstream prompts (Brave, messaging tokens — already after the gate) use the normal immediate-persist path.

Abort flow

The gate's `n/no` branch now:

Aborted. Re-run \`nemoclaw onboard\` to start over.
No credentials were persisted to disk.

Replacing the earlier `credentials reset` workaround copy.

Non-interactive and --dangerously-skip-permissions

Skip the deferral entirely (no gate to flush at). They use the existing immediate-persist path unchanged.

Scope

Touches only `src/lib/onboard.ts` (+90 / −11). The other `saveCredential` call sites (`configureWebSearch`, `setupMessagingChannels`, `sandbox-channels`, `sandbox-config`) already run after the gate and don't need deferral.

Testing

  • `npx vitest run test/onboard.test.ts` — 134 tests pass, no regressions in the non-deferred or resume paths.
  • `npm run build:cli` + `npm run typecheck:cli` clean.

The defer helpers themselves are module-scoped with no test seams that don't require stdin simulation; their correctness is covered by the existing onboard integration tests which exercise both the interactive (deferral on) and non-interactive (deferral off) code paths.

Checklist

  • Follows Conventional Commits
  • Commit is signed (SSH)
  • DCO Signed-off-by trailer present

Signed-off-by: latenighthackathon latenighthackathon@users.noreply.github.com

Summary by CodeRabbit

  • Enhancements

    • Onboarding now holds staged credential values in memory until you confirm at review; helper functions are available to manage this lifecycle.
    • Deferral applies only to interactive, non-skipped flows; staging is cleared when navigating provider-selection retries so stale keys aren’t reused.
    • NVIDIA key prompts follow the deferred/staged flow.
  • Bug Fixes

    • Review messaging and post-review persistence now accurately reflect whether a key is held in memory or saved to disk.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds deferred credential persistence to onboarding: credentials can be staged in-memory (via env vars) instead of written to credentials.json, with helpers to begin, flush, discard, and end deferral; integrates into provider selection, review/confirm gate, and inference setup flows.

Changes

Cohort / File(s) Summary
Onboard & Deferred Credentials
src/lib/onboard.ts
Implements deferred credential persistence: replaceNamedCredential can stage validated keys in process.env and record (envName → stagedValue) in a pending set when deferral is armed. Adds and exports helpers: beginDeferredCredentialPersistence, flushDeferredCredentials, discardDeferredCredentials, endDeferredCredentialPersistence. Updates setupNim to discard staged env var on back navigation and route NVIDIA key prompt through deferred path. onboard now arms deferral only for interactive, non-dangerouslySkipPermissions flows, flushes pending credentials after review approval before running setupInference, discards on rejection, and always disarms in a finally block. Review-gate messaging updated to label keys as “held in memory … on confirm” or “stored.”

Sequence Diagram

sequenceDiagram
    actor User
    participant Onboard as Onboard Flow
    participant Deferral as Deferral Manager
    participant Storage as Storage Layer

    User->>Onboard: Start interactive onboarding / provider selection
    Onboard->>Deferral: beginDeferredCredentialPersistence()
    activate Deferral

    loop Credential entry / provider retries
        User->>Onboard: Enter credential (or go Back)
        Onboard->>Deferral: replaceNamedCredential(name, value)
        Deferral->>Storage: set process.env (record name in pending set)
        alt User navigates Back
            Onboard->>Deferral: discardDeferredCredentials() / discardDeferredCredential(name)
            Deferral->>Storage: clear staged env var
        end
    end

    Onboard->>User: Show review/confirmation
    alt User Confirms
        User->>Onboard: Confirm
        Onboard->>Deferral: flushDeferredCredentials()
        Deferral->>Storage: persist pending entries to credentials.json (only if env still matches staged values)
        Onboard->>Onboard: setupInference() (reads persisted values)
        Onboard->>Deferral: endDeferredCredentialPersistence()
    else User Declines / Aborts
        User->>Onboard: Decline / Abort
        Onboard->>Deferral: discardDeferredCredentials()
        Deferral->>Deferral: clear pending set (no disk writes)
        Onboard->>Deferral: endDeferredCredentialPersistence()
    end

    deactivate Deferral
    Onboard->>User: Complete or Abort
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

🐇 I tuck the keys beneath a leaf so mild,
Held in memory, patient and styled.
Backtrack a hop, or give a cheer—
Flush to the vault, or disappear.
A careful rabbit guards each file 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(onboard): defer credential persistence until the review gate confirms' directly and specifically summarizes the main change—deferring credential persistence to the review gate confirmation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/onboard.ts`:
- Around line 779-783: The issue is that deferCredentialPersistence adds envName
to pendingDeferredCredentials and process.env immediately, but nothing removes
it when setupNim() returns to selectionLoop (so abandoned choices get flushed by
flushDeferredCredentials()). Fix by either moving the additions until the user
confirms the provider, or by removing staged creds when the user backs out:
ensure selectionLoop or the back/navigation path calls
pendingDeferredCredentials.delete(envName) and delete process.env[envName] for
the abandoned envName (or clear any previously staged envName before adding a
new one). Update the code paths around deferCredentialPersistence,
pendingDeferredCredentials, process.env[envName], setupNim(), and selectionLoop
so only the final confirmed credential remains for flushDeferredCredentials().
- Around line 3386-3388: The message always claims the API key is "stored in
~/.nemoclaw/credentials.json" even when credentials are still
deferred/in-memory; change the construction of apiKeyLine to reflect actual
persistence state by checking whether credentials have been flushed (use the
deferred-mode flag or call/inspect
flushDeferredCredentials()/deferredCredentials state) and render either "stored
in ~/.nemoclaw/credentials.json" when persisted or "stored in memory (will be
written after flush/confirmation)" when still deferred; update the apiKeyLine
assignment (referencing apiKeyLine, credentialEnv, provider, and
flushDeferredCredentials()) accordingly.
- Around line 6718-6725: The abort path currently always calls
discardDeferredCredentials() and prints “No credentials were persisted to disk,”
which is incorrect if an earlier iteration already called
flushDeferredCredentials(); fix by tracking whether any deferred credentials
were flushed (e.g., a boolean like deferredCredentialsFlushed set when
flushDeferredCredentials() runs) and change the abort logic to: if
deferredCredentialsFlushed is false, call discardDeferredCredentials() and print
the existing “No credentials were persisted” message; if true, discard only
remaining deferred writes (if needed) but print a safe message such as “Abort:
previously confirmed credentials may have been persisted; remaining deferred
credentials were discarded” to avoid the misleading guarantee. Ensure you
reference and update the logic around discardDeferredCredentials() and
flushDeferredCredentials() and the abort console.log outputs.
- Around line 6637-6640: The NVIDIA API key is being saved immediately because
setupNim() calls ensureApiKey() which uses saveCredential() directly, bypassing
the deferral you intended with deferInferenceCredentials and
beginDeferredCredentialPersistence(); modify the flow so ensureApiKey() (or its
caller in setupNim()) respects the deferral flag: either start
beginDeferredCredentialPersistence() earlier in setupNim() before ensureApiKey()
runs, or add a parameter to ensureApiKey() to avoid calling saveCredential()
directly and instead enqueue the credential with the deferred persistence
mechanism when deferInferenceCredentials is true; update references to
saveCredential(), ensureApiKey(), setupNim(), and
beginDeferredCredentialPersistence() accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fac2c1b-6a42-40b5-aac0-771d47f01fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 68fa01c and 2230538.

📒 Files selected for processing (2)
  • src/lib/onboard.ts
  • test/onboard.test.ts

Comment thread src/lib/onboard.ts
Comment thread src/lib/onboard.ts
Comment thread src/lib/onboard.ts
Comment thread src/lib/onboard.ts Outdated
@wscurran wscurran added NemoClaw CLI refactor PR restructures code without intended behavior change labels Apr 22, 2026
@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR that proposes a way to refactor credential persistence — this could help provide a more secure configuration for credentials.


Related open PRs:

@latenighthackathon latenighthackathon force-pushed the refactor/defer-credential-persist branch from 2230538 to b20e3b3 Compare April 24, 2026 03:29
@copy-pr-bot

copy-pr-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
src/lib/onboard.ts (3)

6812-6827: ⚠️ Potential issue | 🟡 Minor

Abort message can overstate guarantees after prior flush.

If an earlier loop iteration already flushed deferred credentials, this path still prints “No credentials were persisted to disk.” That statement can be false.

Proposed fix
-    let forceProviderSelection = false;
+    let forceProviderSelection = false;
+    let deferredCredentialsFlushed = false;
@@
         if (answer === "n" || answer === "no") {
@@
-          console.log("  No credentials were persisted to disk.");
+          if (deferredCredentialsFlushed) {
+            console.log("  Remaining deferred credentials were discarded.");
+            console.log("  Note: previously confirmed credentials may already be on disk.");
+          } else {
+            console.log("  No credentials were persisted to disk.");
+          }
           process.exit(0);
         }
       }
@@
       if (deferInferenceCredentials) {
         flushDeferredCredentials();
+        deferredCredentialsFlushed = true;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6812 - 6827, The abort path prints "No
credentials were persisted to disk." even if a prior iteration already called
flushDeferredCredentials(); introduce or check a flag that tracks whether
deferred credentials have been flushed (e.g., deferredCredentialsFlushed) and
only call the “No credentials were persisted to disk.” message when no flush has
occurred, or change the messaging to reflect that credentials may have been
persisted earlier; update the logic around discardDeferredCredentials(),
flushDeferredCredentials(), and deferInferenceCredentials to consult this flag
so the printed statement accurately reflects whether any flush happened.

6726-6734: ⚠️ Potential issue | 🟠 Major

Deferral is armed, but NVIDIA key entry still bypasses it.

Deferral is enabled here, but Line 4376 still uses ensureApiKey() in the NVIDIA path, which is outside this deferred write path. That leaves a pre-gate persistence path for NVIDIA credentials.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6726 - 6734, The NVIDIA credential branch
still calls ensureApiKey(), causing immediate persistence outside the deferred
flow started by deferInferenceCredentials/beginDeferredCredentialPersistence;
update the NVIDIA path to use the deferred credential flow instead of
ensureApiKey(): either call the credential collection function that queues
writes into the deferred store (or create/route through a new
ensureDeferredApiKey helper) so writes go through the same deferred persistence
mechanism controlled by deferInferenceCredentials and finalized at the review
gate, and remove any direct calls that persist to ~/.nemoclaw/credentials.json
from the NVIDIA branch.

757-761: ⚠️ Potential issue | 🟠 Major

Deferred staging can persist abandoned provider credentials.

Line 760 adds every entered env key to pendingDeferredCredentials, but abandoned selections are never evicted before flushDeferredCredentials(). This can write credentials that are no longer part of the final confirmed selection.

Proposed fix
     if (deferCredentialPersistence) {
+      // Keep only the current in-progress inference credential staged.
+      // If the user backs out and chooses a different provider, do not
+      // retain abandoned staged credentials for a later flush.
+      for (const stagedEnvName of [...pendingDeferredCredentials]) {
+        if (stagedEnvName !== envName) {
+          pendingDeferredCredentials.delete(stagedEnvName);
+          delete process.env[stagedEnvName];
+        }
+      }
       // Hold in memory + process.env; flushDeferredCredentials() will write
       // to ~/.nemoclaw/credentials.json after the review gate confirms.
       pendingDeferredCredentials.add(envName);
       process.env[envName] = key;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 757 - 761, pendingDeferredCredentials
currently adds every envName when deferCredentialPersistence is true, which
leaves abandoned selections in the set and causes flushDeferredCredentials() to
persist stale credentials; fix by only adding envName to
pendingDeferredCredentials when the provider/credential is actually confirmed
(i.e., move the add(envName) into the confirmation/acceptance path) or ensure
every abort/cancel/change flow removes it (call
pendingDeferredCredentials.delete(envName) and delete process.env[envName]) so
only confirmed envNames remain for flushDeferredCredentials; update the code
paths around deferCredentialPersistence, pendingDeferredCredentials, envName and
flushDeferredCredentials to implement one of these strategies.
🧹 Nitpick comments (1)
src/lib/onboard.ts (1)

734-736: Scrub staged env vars when discarding deferred credentials.

discardDeferredCredentials() disarms and clears the set, but leaves staged secret values in process.env for the remainder of the process. Clearing those env vars is safer and keeps behavior robust if this function is reused without immediate process exit.

Proposed fix
 function discardDeferredCredentials() {
+  for (const envName of pendingDeferredCredentials) {
+    delete process.env[envName];
+  }
   pendingDeferredCredentials.clear();
   deferCredentialPersistence = false;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 734 - 736, discardDeferredCredentials
currently clears pendingDeferredCredentials and flips deferCredentialPersistence
but leaves any staged secret values in process.env; update
discardDeferredCredentials to iterate the pendingDeferredCredentials set (e.g.,
for each key in pendingDeferredCredentials) and delete those entries from
process.env before clearing the set and setting deferCredentialPersistence =
false so staged env vars are scrubbed from the process when credentials are
discarded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/onboard.ts`:
- Around line 6812-6827: The abort path prints "No credentials were persisted to
disk." even if a prior iteration already called flushDeferredCredentials();
introduce or check a flag that tracks whether deferred credentials have been
flushed (e.g., deferredCredentialsFlushed) and only call the “No credentials
were persisted to disk.” message when no flush has occurred, or change the
messaging to reflect that credentials may have been persisted earlier; update
the logic around discardDeferredCredentials(), flushDeferredCredentials(), and
deferInferenceCredentials to consult this flag so the printed statement
accurately reflects whether any flush happened.
- Around line 6726-6734: The NVIDIA credential branch still calls
ensureApiKey(), causing immediate persistence outside the deferred flow started
by deferInferenceCredentials/beginDeferredCredentialPersistence; update the
NVIDIA path to use the deferred credential flow instead of ensureApiKey():
either call the credential collection function that queues writes into the
deferred store (or create/route through a new ensureDeferredApiKey helper) so
writes go through the same deferred persistence mechanism controlled by
deferInferenceCredentials and finalized at the review gate, and remove any
direct calls that persist to ~/.nemoclaw/credentials.json from the NVIDIA
branch.
- Around line 757-761: pendingDeferredCredentials currently adds every envName
when deferCredentialPersistence is true, which leaves abandoned selections in
the set and causes flushDeferredCredentials() to persist stale credentials; fix
by only adding envName to pendingDeferredCredentials when the
provider/credential is actually confirmed (i.e., move the add(envName) into the
confirmation/acceptance path) or ensure every abort/cancel/change flow removes
it (call pendingDeferredCredentials.delete(envName) and delete
process.env[envName]) so only confirmed envNames remain for
flushDeferredCredentials; update the code paths around
deferCredentialPersistence, pendingDeferredCredentials, envName and
flushDeferredCredentials to implement one of these strategies.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 734-736: discardDeferredCredentials currently clears
pendingDeferredCredentials and flips deferCredentialPersistence but leaves any
staged secret values in process.env; update discardDeferredCredentials to
iterate the pendingDeferredCredentials set (e.g., for each key in
pendingDeferredCredentials) and delete those entries from process.env before
clearing the set and setting deferCredentialPersistence = false so staged env
vars are scrubbed from the process when credentials are discarded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0dab8c86-4916-4d1f-a176-9ae95e9b9757

📥 Commits

Reviewing files that changed from the base of the PR and between 2230538 and b20e3b3.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/lib/onboard.ts (2)

6876-6883: ⚠️ Potential issue | 🟠 Major

Abort can over-promise after a previously confirmed retry.

If an earlier loop iteration already hit flushDeferredCredentials(), aborting here still says “No credentials were persisted to disk.” That becomes false as soon as any previous retry was confirmed and flushed.

Also applies to: 6886-6891

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6876 - 6883, The abort messaging incorrectly
always claims “No credentials were persisted to disk.” even when a prior retry
already called flushDeferredCredentials(); update the abort branches (the block
that currently calls discardDeferredCredentials() and the similar block at
6886-6891) to check whether deferred credentials were already flushed and
conditionally log the correct message: if flushDeferredCredentials() (or a
boolean flag set when flushDeferredCredentials() runs) indicates credentials
were written, log that credentials were persisted and provide guidance;
otherwise call discardDeferredCredentials() and log that no credentials were
persisted. Ensure you reference and use the existing functions
flushDeferredCredentials() and discardDeferredCredentials() (or set/read a
flushed flag where flushDeferredCredentials() runs) so the message accurately
reflects state before calling process.exit(0).

715-727: ⚠️ Potential issue | 🟠 Major

Backing out to another provider can still flush the abandoned key.

The env name is staged as soon as the key is entered, and flushDeferredCredentials() later persists every staged name. I still don't see any "back" / "selection" path removing the abandoned env name or process.env value before the user picks a different provider, so confirming the second choice can write both credentials.

Also applies to: 757-761

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 715 - 727, The current
flushDeferredCredentials() unconditionally persists any env names in
pendingDeferredCredentials, allowing an abandoned/previous provider's key to be
saved; fix by tracking the staged credential value when staging (store a map
stagedCredentialValues: Map<string,string> alongside
pendingDeferredCredentials), then in flushDeferredCredentials() only call
saveCredential(envName, value) if process.env[envName] ===
stagedCredentialValues.get(envName) and the value is non-empty; also ensure any
“back”/“selection” or cancellation paths remove the envName from
pendingDeferredCredentials and stagedCredentialValues (and clear
process.env[envName] if appropriate) so abandoned entries cannot be
flushed—refer to flushDeferredCredentials, pendingDeferredCredentials,
saveCredential, endDeferredCredentialPersistence and the code that stages env
names when a key is entered to implement this.
🧹 Nitpick comments (1)
src/lib/onboard.ts (1)

6795-6798: Pair the deferral lifecycle with a finally.

If anything throws between beginDeferredCredentialPersistence() and endDeferredCredentialPersistence(), the module-scoped flag/set stay armed for the rest of the process. Wrapping this retry loop in a try/finally would avoid leaking deferred state into later prompts or tests.

Also applies to: 6916-6918

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6795 - 6798, The code arms deferred
credential persistence via deferInferenceCredentials and calls
beginDeferredCredentialPersistence() but does not guarantee
endDeferredCredentialPersistence() runs on error; wrap the block that runs while
deferInferenceCredentials is true (the retry loop and any logic between
beginDeferredCredentialPersistence() and endDeferredCredentialPersistence()) in
a try/finally so that endDeferredCredentialPersistence() is always called in the
finally clause (also apply the same change to the other occurrence around lines
6916-6918); reference deferInferenceCredentials,
beginDeferredCredentialPersistence, and endDeferredCredentialPersistence to
locate and protect the lifecycle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/onboard.ts`:
- Around line 766-770: The success message is being printed twice because
saveCredential already logs the "~/.nemoclaw/credentials.json" success line;
remove the duplicate prints in the immediate flow by deleting the three
console.log calls after saveCredential (the two blank console.log("") calls and
the console.log(`  Key saved to ~/.nemoclaw/credentials.json (mode 600)`)) in
the block that sets process.env[envName] so only the credential-saving function
emits the message.
- Around line 757-764: Deferred-mode keys are only written to process.env and
pendingDeferredCredentials but selection-time validators still call
getCredential(...) which reads persisted storage, causing new keys to be
invisible; update the credential lookup so staged keys are visible: change
getCredential (the central credential getter used by the selection-time and
remote model validators) to first check process.env[envName] and/or
pendingDeferredCredentials (the set used when deferCredentialPersistence is
true) and return that staged value if present before falling back to persisted
credentials; this ensures validators at selection time and in the remote model
validators see the freshly entered key without waiting for
flushDeferredCredentials().

---

Duplicate comments:
In `@src/lib/onboard.ts`:
- Around line 6876-6883: The abort messaging incorrectly always claims “No
credentials were persisted to disk.” even when a prior retry already called
flushDeferredCredentials(); update the abort branches (the block that currently
calls discardDeferredCredentials() and the similar block at 6886-6891) to check
whether deferred credentials were already flushed and conditionally log the
correct message: if flushDeferredCredentials() (or a boolean flag set when
flushDeferredCredentials() runs) indicates credentials were written, log that
credentials were persisted and provide guidance; otherwise call
discardDeferredCredentials() and log that no credentials were persisted. Ensure
you reference and use the existing functions flushDeferredCredentials() and
discardDeferredCredentials() (or set/read a flushed flag where
flushDeferredCredentials() runs) so the message accurately reflects state before
calling process.exit(0).
- Around line 715-727: The current flushDeferredCredentials() unconditionally
persists any env names in pendingDeferredCredentials, allowing an
abandoned/previous provider's key to be saved; fix by tracking the staged
credential value when staging (store a map stagedCredentialValues:
Map<string,string> alongside pendingDeferredCredentials), then in
flushDeferredCredentials() only call saveCredential(envName, value) if
process.env[envName] === stagedCredentialValues.get(envName) and the value is
non-empty; also ensure any “back”/“selection” or cancellation paths remove the
envName from pendingDeferredCredentials and stagedCredentialValues (and clear
process.env[envName] if appropriate) so abandoned entries cannot be
flushed—refer to flushDeferredCredentials, pendingDeferredCredentials,
saveCredential, endDeferredCredentialPersistence and the code that stages env
names when a key is entered to implement this.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 6795-6798: The code arms deferred credential persistence via
deferInferenceCredentials and calls beginDeferredCredentialPersistence() but
does not guarantee endDeferredCredentialPersistence() runs on error; wrap the
block that runs while deferInferenceCredentials is true (the retry loop and any
logic between beginDeferredCredentialPersistence() and
endDeferredCredentialPersistence()) in a try/finally so that
endDeferredCredentialPersistence() is always called in the finally clause (also
apply the same change to the other occurrence around lines 6916-6918); reference
deferInferenceCredentials, beginDeferredCredentialPersistence, and
endDeferredCredentialPersistence to locate and protect the lifecycle.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 4e8b9770-fb25-41cd-b209-b90d50bf93f2

📥 Commits

Reviewing files that changed from the base of the PR and between b20e3b3 and 408d724.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

Comment thread src/lib/onboard.ts
Comment thread src/lib/onboard.ts
@latenighthackathon latenighthackathon force-pushed the refactor/defer-credential-persist branch from 408d724 to 1104aed Compare April 24, 2026 04:57
@latenighthackathon

latenighthackathon commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 7e89bc18 (dropped the Update-Branch merge so DCO is green) and addressed coderabbit nits.
Cheers!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/lib/onboard.ts (1)

791-795: ⚠️ Potential issue | 🟡 Minor

Avoid double-printing the save banner in the immediate path.

If saveCredential() still owns the interactive credentials.json success output, these lines will print the same status twice. Please verify the helper and keep the message in one place only.

#!/bin/bash
set -euo pipefail

echo "== saveCredential definition / output =="
rg -n -C4 '\bsaveCredential\s*\(|credentials\.json' src/lib/credentials.ts

echo
echo "== immediate-save branch in onboard.ts =="
sed -n '788,796p' src/lib/onboard.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 791 - 795, The code prints the
credentials.json success banner twice: once inside saveCredential(...) and again
immediately after in the onboard immediate-save branch (the console.log block
that references ~/.nemoclaw/credentials.json); inspect the saveCredential
implementation in credentials.ts to see if it already logs the success message,
then remove the duplicate output by either deleting the console.log(...) lines
here (the immediate-save block that uses envName and key) or suppressing logging
inside saveCredential so only one place logs the success—ensure only one of the
two (saveCredential or the onboard immediate-save branch) emits the
credentials.json save banner.
🧹 Nitpick comments (1)
src/lib/onboard.ts (1)

6861-7007: Extract the deferred review-gate orchestration out of onboard().

This patch adds more stateful branching to an already complexity-suppressed retry loop. Pulling the defer/flush/discard/summary flow into a dedicated helper would make this path much easier to reason about and test.

As per coding guidelines, TypeScript files must maintain cyclomatic complexity limit of 20, ratcheting down to 15. Enforced by ESLint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6861 - 7007, The review gate and
deferred-credential orchestration (calculation of deferInferenceCredentials,
beginDeferredCredentialPersistence(), the summary/confirmation prompt block that
calls promptValidatedSandboxName(), formatOnboardConfigSummary(),
promptOrDefault(), discardDeferredCredentials(),
haveDeferredCredentialsBeenFlushed(), flushDeferredCredentials(), and
endDeferredCredentialPersistence()) should be extracted from onboard() into a
single helper (e.g., handleDeferredCredentialReviewGate or
confirmInferenceCredentials) that accepts the mutable pieces it needs (provider,
model, credentialEnv, webSearchConfig, selectedMessagingChannels, sandboxName,
dangerouslySkipPermissions, isNonInteractive, etc.) and returns the updated
sandboxName plus a flag indicating whether to proceed or abort; replace the
inline block with a call to this helper, ensure
beginDeferredCredentialPersistence()/endDeferredCredentialPersistence() are
paired inside the helper, preserve the priorFlush logic and process.exit
behavior by returning an abort signal instead of exiting deep in onboarding, and
add unit tests for the new helper to reduce onboard() cyclomatic complexity to
satisfy ESLint limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/onboard.ts`:
- Around line 791-795: The code prints the credentials.json success banner
twice: once inside saveCredential(...) and again immediately after in the
onboard immediate-save branch (the console.log block that references
~/.nemoclaw/credentials.json); inspect the saveCredential implementation in
credentials.ts to see if it already logs the success message, then remove the
duplicate output by either deleting the console.log(...) lines here (the
immediate-save block that uses envName and key) or suppressing logging inside
saveCredential so only one place logs the success—ensure only one of the two
(saveCredential or the onboard immediate-save branch) emits the credentials.json
save banner.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 6861-7007: The review gate and deferred-credential orchestration
(calculation of deferInferenceCredentials, beginDeferredCredentialPersistence(),
the summary/confirmation prompt block that calls promptValidatedSandboxName(),
formatOnboardConfigSummary(), promptOrDefault(), discardDeferredCredentials(),
haveDeferredCredentialsBeenFlushed(), flushDeferredCredentials(), and
endDeferredCredentialPersistence()) should be extracted from onboard() into a
single helper (e.g., handleDeferredCredentialReviewGate or
confirmInferenceCredentials) that accepts the mutable pieces it needs (provider,
model, credentialEnv, webSearchConfig, selectedMessagingChannels, sandboxName,
dangerouslySkipPermissions, isNonInteractive, etc.) and returns the updated
sandboxName plus a flag indicating whether to proceed or abort; replace the
inline block with a call to this helper, ensure
beginDeferredCredentialPersistence()/endDeferredCredentialPersistence() are
paired inside the helper, preserve the priorFlush logic and process.exit
behavior by returning an abort signal instead of exiting deep in onboarding, and
add unit tests for the new helper to reduce onboard() cyclomatic complexity to
satisfy ESLint limits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a95e3806-044a-4a2f-b1d2-64fa1a7adbc3

📥 Commits

Reviewing files that changed from the base of the PR and between 408d724 and 1104aed.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request Apr 24, 2026
Addresses CodeRabbit concerns from the 04-24 review on NVIDIA#2226:

- `pendingDeferredCredentials` is now `Map<string, string>` (envName → staged
  value). `flushDeferredCredentials()` only persists entries where
  `process.env[envName]` still equals the staged value, so a back-nav path
  that abandoned a provider's key cannot flush it alongside the final
  selection. Defense-in-depth with the existing `discardStagedCredential`
  path in `setupNim`'s `selectionLoop`.
- Wrap the inference retry loop in `try/finally` so
  `endDeferredCredentialPersistence()` always disarms the module-scoped
  deferral flag, even if the loop throws. Prevents leaking state into later
  prompts or tests.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon latenighthackathon force-pushed the refactor/defer-credential-persist branch from 05a3645 to cd1aa2a Compare April 24, 2026 13:10
@latenighthackathon

latenighthackathon commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit nits - Cheers!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/lib/onboard.ts (1)

720-745: ⚠️ Potential issue | 🔴 Critical

Staged keys still need to be visible to every later lookup path.

These helpers now rely on process.env/pendingDeferredCredentials, but this file still re-reads credentials via getCredential(...) during selection validation and via hydrateCredentialEnv() at the top of setupInference(). If src/lib/credentials.ts#getCredential is still reading persisted storage, a freshly entered key stays invisible before confirm, and a post-confirm "retry credential" inside setupInference() will fall back to the old saved key on the next loop. Either make the shared lookup prefer staged env values or stop deferring once the first review gate has been accepted.

#!/bin/bash
set -euo pipefail

echo "=== credential lookup implementation ==="
rg -n -C4 '\bfunction getCredential\b|\bfunction saveCredential\b|process\.env|credentials\.json' src/lib/credentials.ts

echo
echo "=== onboard call sites that re-read credentials after staging ==="
rg -n -C2 '\bgetCredential\(|\bhydrateCredentialEnv\(' src/lib/onboard.ts

Expected result: getCredential() (or a shared helper used by these call sites) should prefer staged process.env values before persisted storage while deferral is active.

Also applies to: 795-803

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 720 - 745, The staged credential values
aren't being honored by later lookups in setupInference and selection
validation; update the shared lookup logic so getCredential (or the helper used
by hydrateCredentialEnv and setupInference) prefers staged values when
deferCredentialPersistence is active by first checking process.env[envName] and
pendingDeferredCredentials for a non-empty stagedValue before falling back to
persisted storage, or alternatively clear deferCredentialPersistence (call
endDeferredCredentialPersistence()) immediately after the first review/confirm
gate is accepted so subsequent getCredential/hydrateCredentialEnv calls see the
freshly staged value; touch the functions getCredential, hydrateCredentialEnv,
setupInference, flushDeferredCredentials, and endDeferredCredentialPersistence
to implement this consistent precedence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/onboard.ts`:
- Around line 720-745: The staged credential values aren't being honored by
later lookups in setupInference and selection validation; update the shared
lookup logic so getCredential (or the helper used by hydrateCredentialEnv and
setupInference) prefers staged values when deferCredentialPersistence is active
by first checking process.env[envName] and pendingDeferredCredentials for a
non-empty stagedValue before falling back to persisted storage, or alternatively
clear deferCredentialPersistence (call endDeferredCredentialPersistence())
immediately after the first review/confirm gate is accepted so subsequent
getCredential/hydrateCredentialEnv calls see the freshly staged value; touch the
functions getCredential, hydrateCredentialEnv, setupInference,
flushDeferredCredentials, and endDeferredCredentialPersistence to implement this
consistent precedence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 19323d52-148c-40bf-a3d1-aea88d645aea

📥 Commits

Reviewing files that changed from the base of the PR and between 1104aed and cd1aa2a.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request Apr 25, 2026
Addresses CodeRabbit concerns from the 04-24 review on NVIDIA#2226:

- `pendingDeferredCredentials` is now `Map<string, string>` (envName → staged
  value). `flushDeferredCredentials()` only persists entries where
  `process.env[envName]` still equals the staged value, so a back-nav path
  that abandoned a provider's key cannot flush it alongside the final
  selection. Defense-in-depth with the existing `discardStagedCredential`
  path in `setupNim`'s `selectionLoop`.
- Wrap the inference retry loop in `try/finally` so
  `endDeferredCredentialPersistence()` always disarms the module-scoped
  deferral flag, even if the loop throws. Prevents leaking state into later
  prompts or tests.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon latenighthackathon force-pushed the refactor/defer-credential-persist branch from cd1aa2a to 141418f Compare April 25, 2026 05:06
latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request Apr 26, 2026
Addresses CodeRabbit concerns from the 04-24 review on NVIDIA#2226:

- `pendingDeferredCredentials` is now `Map<string, string>` (envName → staged
  value). `flushDeferredCredentials()` only persists entries where
  `process.env[envName]` still equals the staged value, so a back-nav path
  that abandoned a provider's key cannot flush it alongside the final
  selection. Defense-in-depth with the existing `discardStagedCredential`
  path in `setupNim`'s `selectionLoop`.
- Wrap the inference retry loop in `try/finally` so
  `endDeferredCredentialPersistence()` always disarms the module-scoped
  deferral flag, even if the loop throws. Prevents leaking state into later
  prompts or tests.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon latenighthackathon force-pushed the refactor/defer-credential-persist branch from 141418f to a7806b0 Compare April 26, 2026 18:45
… confirms

Follow-up to NVIDIA#2221 addressing the remaining credential-persistence window
CodeRabbit flagged: setupNim's credential prompt writes to
~/.nemoclaw/credentials.json via saveCredential as soon as the user types
a key, so a later abort at the review gate still left that key on disk.

Introduce a module-level defer flag so credential-prompt writes are held
in memory until the gate confirms, plus follow-up fixes addressing
CodeRabbit review on b20e3b3.

Mechanism

- beginDeferredCredentialPersistence() arms the flag, clears the pending
  set, resets the flush-tracking bit. Called once at the top of the
  inference retry loop in onboard(), only when the review gate will
  actually fire (interactive + not --dangerously-skip-permissions).
- replaceNamedCredential(), when the flag is armed, sets process.env
  and adds the env name to pendingDeferredCredentials, skipping the
  saveCredential call. Prints "Key held in memory; will be saved to
  credentials.json on confirm." instead of the usual "Key saved".
- flushDeferredCredentials() iterates the pending set, calls
  saveCredential(envName, process.env[envName]) for each, clears the
  set, and marks deferredCredentialsFlushed so a subsequent abort after
  a confirmed-then-failed retry iteration tells the truth.
- discardDeferredCredentials() now also clears the pending env vars
  from process.env, not just the set. Called on gate abort.
- endDeferredCredentialPersistence() clears, disarms, resets the flush
  bit. Called after the retry loop breaks successfully so downstream
  prompts (Brave, messaging tokens) use the normal immediate path.
- discardStagedCredential(envName) removes a single envName from the
  pending set and process.env. Used when setupNim goes back to
  selectionLoop after a credential was staged for an abandoned provider.

setupNim changes (close CodeRabbit Majors at lines 761 and 6798)

- In deferred mode only, route the NVIDIA API key prompt through
  replaceNamedCredential (respects the flag) instead of ensureApiKey
  (which writes immediately). Non-deferred mode still calls ensureApiKey
  so existing mocks and single-path flows are unchanged. The onboarding
  box UI is preserved inline; validateNvidiaApiKeyValue runs the same
  starts-with-nvapi- check ensureApiKey did.
- Track stagedCredentialEnvThisIteration across selectionLoop iterations
  and discardStagedCredential() at the top of each new iteration so a
  user who enters key A for provider X, navigates back, then picks
  provider Y with key B only has B in pendingDeferredCredentials when
  the gate flushes.

formatOnboardConfigSummary changes (close CodeRabbit Minor at line 3378)

- New credentialStorageState parameter distinguishes "deferred" (key
  held in memory) from "persisted" (already on disk). The summary now
  says "held in memory — will be saved to ~/.nemoclaw/credentials.json
  on confirm" when the key is still pending so the gate's promise
  matches reality. The call site passes the state based on
  pendingDeferredCredentials membership.

Abort flow (closes CodeRabbit Minor at line 6883)

- The gate's n/no branch now checks haveDeferredCredentialsBeenFlushed()
  before printing its after-message. If no flush ever happened it says
  "No credentials were persisted to disk." as before. If an earlier
  retry iteration flushed and this iteration aborted, it acknowledges
  that "credentials confirmed in an earlier attempt are already in
  credentials.json" and points to nemoclaw credentials reset <KEY>.

Non-interactive and --dangerously-skip-permissions flows skip the
deferral entirely and use the existing immediate-persist path.

Scope

Touches only onboard.ts. The other saveCredential call sites
(configureWebSearch, setupMessagingChannels, sandbox-channels,
sandbox-config) already run after the gate and dont need deferral.

Tests

- test/onboard.test.ts — 135 tests pass.
- test/onboard-selection.test.ts — 33 tests pass (verifies setupNim
  back-nav + NVIDIA Endpoints path).
- Typecheck clean.

Stacks on NVIDIA#2221 — depends on the gate and formatOnboardConfigSummary
landing first.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
Addresses CodeRabbit concerns from the 04-24 review on NVIDIA#2226:

- `pendingDeferredCredentials` is now `Map<string, string>` (envName → staged
  value). `flushDeferredCredentials()` only persists entries where
  `process.env[envName]` still equals the staged value, so a back-nav path
  that abandoned a provider's key cannot flush it alongside the final
  selection. Defense-in-depth with the existing `discardStagedCredential`
  path in `setupNim`'s `selectionLoop`.
- Wrap the inference retry loop in `try/finally` so
  `endDeferredCredentialPersistence()` always disarms the module-scoped
  deferral flag, even if the loop throws. Prevents leaking state into later
  prompts or tests.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon latenighthackathon force-pushed the refactor/defer-credential-persist branch from a7806b0 to 93d15db Compare April 27, 2026 22:33
@latenighthackathon

Copy link
Copy Markdown
Contributor Author

Closing — #2554 (host-side credential hardening) made saveCredential env-only, so the deferred-disk-write machinery this PR adds (pendingDeferredCredentials / flushDeferredCredentials / haveDeferredCredentialsBeenFlushed) is no longer needed — credentials never touch disk during onboard now, and the abort path naturally drops them. Cheers!

@latenighthackathon latenighthackathon deleted the refactor/defer-credential-persist branch April 29, 2026 07:40
@wscurran wscurran added area: cli Command line interface, flags, terminal UX, or output and removed NemoClaw CLI labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Command line interface, flags, terminal UX, or output refactor PR restructures code without intended behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants