Skip to content

fix(onboard): pre-select suggested policy presets in visual display#777

Merged
cv merged 1 commit into
NVIDIA:mainfrom
temrjan:fix/preselect-suggested-policy-presets
Mar 31, 2026
Merged

fix(onboard): pre-select suggested policy presets in visual display#777
cv merged 1 commit into
NVIDIA:mainfrom
temrjan:fix/preselect-suggested-policy-presets

Conversation

@temrjan

@temrjan temrjan commented Mar 24, 2026

Copy link
Copy Markdown

Summary

Fixes #706

Suggested policy presets (e.g. npm, pypi) now display with a filled bullet instead of an empty , matching the prompt behavior that applies them by default.

Details

Before (all presets show ):

○ npm — npm and Yarn registry access (suggested)
○ pypi — Python Package Index (PyPI) access (suggested)

After (suggested presets show ):

● npm — npm and Yarn registry access (suggested)
● pypi — Python Package Index (PyPI) access (suggested)

One-line change in bin/lib/onboard.js: include suggestions array in the marker condition alongside applied.

Test plan

  • Verified npm test — no new test failures
  • Visual: run nemoclaw onboard, observe step 7 — suggested presets should show

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Policy preset suggestions now display as selected in the interactive listing before being officially applied. This change improves visual feedback, making it clearer to users which presets are recommended for their configuration.

Signed-off-by: Temrjan x.temrjan@gmail.com

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The selection indicator for policy presets in the interactive listing now displays suggested presets as selected alongside already-applied presets. This updates the visual status indicator so that presets marked as suggested appear selected before actual application, aligning the UI with the intended behavior.

Changes

Cohort / File(s) Summary
Policy Preset Selection UI
bin/lib/onboard.js
Modified selection marker logic to treat presets as selected if they exist in either the applied or suggestions lists, ensuring suggested presets display as visually selected in the interactive prompt.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐰 A whisper through the warren fair,
Where suggested presets now declare,
Their selection mark shines bright and true,
Selected before applied—a cleaner view!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.69% 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
Title check ✅ Passed The title accurately describes the main change: enabling visual pre-selection of suggested policy presets in the onboarding UI display.
Linked Issues check ✅ Passed The PR directly addresses issue #706 by modifying the selection marker logic to display suggested presets as selected (filled bullet) before application.
Out of Scope Changes check ✅ Passed The one-line change in bin/lib/onboard.js is narrowly scoped to the stated objective and does not introduce unrelated modifications.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@prekshivyas prekshivyas 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.

Clean one-liner, all CI green. LGTM.

@brandonpelfrey

Copy link
Copy Markdown
Collaborator

Hey @temrjan — merging is blocked because commit 6c1434c ("fix(onboard): pre-select suggested policy presets in visual display") isn't signed. The repo requires verified signatures on all commits.

Suggestion:

  # 1. Configure git to sign commits with your GitHub SSH key or GPG key
  #    SSH signing (easier):
  git config --global gpg.format ssh
  git config --global user.signingkey ~/.ssh/id_ed25519  # or whichever key is on your GitHub account
  git config --global commit.gpgsign true

  # 2. Amend the unsigned commit to re-sign it
  git checkout fix/preselect-suggested-policy-presets
  git commit --amend --no-edit -S

  # 3. Force-push the branch
  git push --force-with-lease

Key thing: Make sure the signing key (SSH or GPG) is added to your GitHub account under Settings → SSH and GPG keys so GitHub can verify it.

@temrjan temrjan force-pushed the fix/preselect-suggested-policy-presets branch from 62866a3 to a0bc8ca Compare March 25, 2026 17:50

@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: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
bin/lib/nim.js (1)

191-205: ⚠️ Potential issue | 🟠 Major

The new by-name status helper can't report health correctly for non-default ports.

nimStatusByName() is now the generic status entry point, but its health probe is still hardcoded to localhost:8000. Any container started on another host port will be reported unhealthy, and another service on 8000 can produce a false healthy. This helper needs the mapped port as input or should read it from docker inspect.

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

In `@bin/lib/nim.js` around lines 191 - 205, nimStatusByName currently probes
health against a hardcoded localhost:8000; change it to derive the actual mapped
host port for the container and probe that address instead. In the running
branch of nimStatusByName, use docker inspect (e.g. read .NetworkSettings.Ports
or use `docker port` for the container name) to obtain the host port mapped for
container port 8000, build the host URL (host:mappedPort) and then curl that URL
(/v1/models) to determine health; update the health check logic (the `health`
and `healthy` calculation) to use the resolved host:port instead of
localhost:8000. Ensure this handles missing mappings gracefully (fall back to
unhealthy) and keeps ignoreError behavior.
nemoclaw-blueprint/policies/openclaw-sandbox.yaml (1)

160-175: ⚠️ Potential issue | 🟠 Major

node is still too broad for a “notifications only” egress path.

Allowing /usr/local/bin/node means arbitrary JavaScript run by the agent can hit Telegram/Discord whenever notification credentials are present, so this is broader than the comment suggests. If the goal is to reserve these endpoints for OpenClaw notifications, scope the allow-list to a dedicated notifier launcher/helper instead of the general-purpose interpreter.

Also applies to: 203-204

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

In `@nemoclaw-blueprint/policies/openclaw-sandbox.yaml` around lines 160 - 175,
The policy currently allows the generic Node.js interpreter under the telegram
egress rule ("telegram" block, "binaries" entry with path /usr/local/bin/node),
which is too broad; replace that generic interpreter with a dedicated notifier
launcher/helper binary (e.g., the OpenClaw notifier binary or wrapper) and
update the "telegram" block's "binaries" entry to point to that specific
executable; ensure any other egress blocks that reference /usr/local/bin/node
are similarly scoped to the dedicated notifier (remove /usr/local/bin/node from
"telegram" and the other referenced blocks and add the specific notifier binary
path instead), and confirm the launcher is the only component allowed to call
the telegram endpoints.
test/onboard-selection.test.js (1)

83-91: ⚠️ Potential issue | 🟠 Major

The spawned test processes inherit host credentials.

Each spawnSync(..., { env: { ...process.env, ... } }) pulls in whatever provider keys are set on the runner. Since setupNim branches on env credentials, a developer/CI machine with NVIDIA_API_KEY, OPENAI_API_KEY, etc. can change the provider list and prompt order, making these assertions non-hermetic. The same ...process.env merge is repeated in the later cases too.

🔒 Make the child env hermetic
-      env: {
-        ...process.env,
-        HOME: tmpDir,
-        PATH: `${fakeBin}:${process.env.PATH || ""}`,
-      },
+      env: {
+        HOME: tmpDir,
+        PATH: `${fakeBin}:${process.env.PATH || ""}`,
+        NVIDIA_API_KEY: "",
+        OPENAI_API_KEY: "",
+        ANTHROPIC_API_KEY: "",
+        GEMINI_API_KEY: "",
+        COMPATIBLE_API_KEY: "",
+        COMPATIBLE_ANTHROPIC_API_KEY: "",
+      },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/onboard-selection.test.js` around lines 83 - 91, The test spawns child
Node processes using spawnSync and merges ...process.env, which leaks host
credentials (e.g., OPENAI_API_KEY) and makes the test non-hermetic; update the
spawnSync env objects (the block that currently uses env: { ...process.env,
HOME: tmpDir, PATH: `${fakeBin}:${process.env.PATH || ""}` }) to construct a
minimal, explicit environment instead of spreading process.env — e.g., set only
HOME: tmpDir, PATH: `${fakeBin}:${process.env.PATH || ""}` (or PATH from
process.env.PATH), and any other minimal runtime keys you actually rely on
(NODE_ENV, CI) — and apply the same change to the other spawnSync cases so no
provider keys are inherited.
bin/lib/onboard.js (2)

1373-1385: ⚠️ Potential issue | 🟠 Major

Keeping an existing sandbox skips the dashboard forward restart.

preflight() tears down the old 18789 forward, but this early return bypasses the forward start block below. Re-running onboard and choosing to keep the existing sandbox leaves the dashboard unreachable even though the flow reports success.

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

In `@bin/lib/onboard.js` around lines 1373 - 1385, The early return when the user
chooses to keep the existing sandbox bypasses the subsequent forward-start logic
that re-establishes the dashboard port torn down by preflight(), so ensure the
forward is started before returning: after the "Keeping existing sandbox."
branch (where sandboxName is returned), invoke the same forward-start sequence
used below (e.g., call the forward.start()/startForward() routine or the
function that handles the "forward start" block) so port 18789 is re-established
for the dashboard; keep sandboxName returned as before but only after running
the forward-start logic (reference symbols: preflight(),
forward.start()/startForward(), sandboxName).

1774-1779: ⚠️ Potential issue | 🟠 Major

The local NIM fallback returns an unusable cloud config.

When nim.waitForNimHealth() fails, this branch logs “Falling back to cloud API” but exits the selection loop with model = null and without selecting or validating a real cloud model. The later inference setup then runs with an inconsistent provider/model pair.

Also applies to: 1789-1794

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

In `@bin/lib/onboard.js` around lines 1774 - 1779, When nim.waitForNimHealth()
fails the code sets model = null and nimContainer = null but does not select or
validate a cloud provider/model, leaving an inconsistent provider/model state;
change the failure branch so it clears nimContainer, forces selection of a cloud
provider and model (e.g., call the existing cloud model selection routine or set
provider = 'cloud' and pick a default from cloudModels), then validate that
provider/model pair with the same validation function used elsewhere (e.g.,
validateProviderModel or selectModelForProvider) and only exit the selection
loop when a valid cloud model is chosen; if validation fails continue the
selection loop or abort with a clear error.
🟡 Minor comments (7)
test/e2e/e2e-cloud-experimental/features/skill/add-sandbox-skill.sh-19-20 (1)

19-20: ⚠️ Potential issue | 🟡 Minor

Fix the fixture path in this usage comment.

The script's actual default is features/skill/fixtures/skill-smoke-template.SKILL.md on Line 38, so the path shown here points readers to the wrong file when they update the smoke template.

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

In `@test/e2e/e2e-cloud-experimental/features/skill/add-sandbox-skill.sh` around
lines 19 - 20, Update the usage comment at the top of add-sandbox-skill.sh that
mentions the default template: the comment currently points to the wrong
fixture; change the path text to the actual default template path used by the
script (features/skill/fixtures/skill-smoke-template.SKILL.md) so SKILL_FILE /
SKILL_BODY documentation matches the behavior in the script.
test/policies.test.js-90-96 (1)

90-96: ⚠️ Potential issue | 🟡 Minor

Restore the previous env var value instead of always deleting it.

At Line 95, unconditional delete can leak state across tests when NEMOCLAW_OPENSHELL_BIN was pre-set by the runner/environment.

Suggested fix
   it("uses the resolved openshell binary when provided by the installer path", () => {
+    const previousOpenshellBin = process.env.NEMOCLAW_OPENSHELL_BIN;
     process.env.NEMOCLAW_OPENSHELL_BIN = "/tmp/fake path/openshell";
     try {
       const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "my-assistant");
       assert.equal(cmd, "'/tmp/fake path/openshell' policy set --policy '/tmp/policy.yaml' --wait 'my-assistant'");
     } finally {
-      delete process.env.NEMOCLAW_OPENSHELL_BIN;
+      if (previousOpenshellBin === undefined) {
+        delete process.env.NEMOCLAW_OPENSHELL_BIN;
+      } else {
+        process.env.NEMOCLAW_OPENSHELL_BIN = previousOpenshellBin;
+      }
     }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/policies.test.js` around lines 90 - 96, The test mutates
process.env.NEMOCLAW_OPENSHELL_BIN and currently unconditionally deletes it in
the finally block, which can leak state; change the test to save the previous
value (e.g., const prev = process.env.NEMOCLAW_OPENSHELL_BIN) before assigning
"/tmp/fake path/openshell" and in the finally block restore it: if prev is
undefined delete process.env.NEMOCLAW_OPENSHELL_BIN else set
process.env.NEMOCLAW_OPENSHELL_BIN = prev; keep the assertion using
policies.buildPolicySetCommand unchanged.
.agents/skills/nemoclaw-reference/references/architecture.md-44-52 (1)

44-52: ⚠️ Potential issue | 🟡 Minor

Reconcile the runtime model in this doc.

This section says the blueprint runtime lives in nemoclaw/src/blueprint/, but the unchanged intro still describes a Python blueprint subprocess, and the lifecycle section below still omits the newly listed rollback phase. Please update those sections together so the architecture reads consistently.

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

In @.agents/skills/nemoclaw-reference/references/architecture.md around lines 44
- 52, Update the architecture doc to consistently reflect the TypeScript
blueprint runtime under nemoclaw/src/blueprint/: remove or replace the outdated
"Python blueprint subprocess" wording in the intro with a clear statement that
the blueprint runtime is implemented in TypeScript in the plugin source tree
(referencing runner.ts, ssrf.ts, snapshot.ts, state.ts), and amend the lifecycle
section to include the missing "rollback" phase and note that runner.ts
implements plan/apply/status/rollback behavior; keep descriptions aligned (e.g.,
mention snapshot.ts handles migration snapshot/restore and state.ts handles
persistent run state) so all mentions match the new runtime model.
test/security-binaries-restriction.test.js-23-29 (1)

23-29: ⚠️ Potential issue | 🟡 Minor

Ignore top-level comments when leaving network_policies.

This exit condition stops scanning on any column-0 text, so a # ... comment between entries can end the section early and let later policy blocks skip the binaries assertion.

Suggested fix
-      if (inNetworkPolicies && /^\S/.test(line) && line.trim() !== "") {
+      if (inNetworkPolicies && /^\S/.test(line) && line.trim() !== "" && !/^\s*#/.test(line)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/security-binaries-restriction.test.js` around lines 23 - 29, The exit
condition for leaving the network_policies block incorrectly treats any column-0
text (including top-level comments) as the end, so update the check in the block
that uses inNetworkPolicies, currentBlock and the /^\S/ test (the if with
inNetworkPolicies && /^\S/.test(line) && line.trim() !== "") to ignore top-level
comments: first compute trimmedLine = line.trim(), and only treat a column-0
line as the section end if trimmedLine is non-empty and does NOT start with '#'
(i.e. skip lines where trimmedLine.startsWith('#')), leaving comment lines
inside network_policies and preventing premature exit so later policy blocks
still get validated for binaries.
test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh-25-26 (1)

25-26: ⚠️ Potential issue | 🟡 Minor

Fix the usage example path.

The documented command drops the e2e-cloud-experimental/ directory, so a copy/paste run from the repo root will miss this script.

Suggested fix
-#   NEMOCLAW_NON_INTERACTIVE=1 NVIDIA_API_KEY=nvapi-... bash test/e2e/test-port8080-conflict.sh
+#   NEMOCLAW_NON_INTERACTIVE=1 NVIDIA_API_KEY=nvapi-... bash test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh` around lines 25 -
26, Update the usage comment at the top of test-port8080-conflict.sh so the
example command includes the correct relative path to this script
(e2e-cloud-experimental/test-port8080-conflict.sh) instead of just
test/e2e/test-port8080-conflict.sh; edit the comment lines near the
shebang/usage block to show: NEMOCLAW_NON_INTERACTIVE=1 NVIDIA_API_KEY=nvapi-...
bash e2e-cloud-experimental/test-port8080-conflict.sh so copying from the repo
root runs the correct script.
test/e2e/e2e-cloud-experimental/test-inference-local-chat.sh-20-21 (1)

20-21: ⚠️ Potential issue | 🟡 Minor

The usage example points at the wrong script name.

The file is test/e2e/e2e-cloud-experimental/test-inference-local-chat.sh, but the comment still tells users to run demo-inference-local-chat.sh.

Suggested fix
-#   bash test/e2e/demo-inference-local-chat.sh
+#   bash test/e2e/e2e-cloud-experimental/test-inference-local-chat.sh
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/e2e-cloud-experimental/test-inference-local-chat.sh` around lines 20
- 21, Update the usage comment that currently references
"demo-inference-local-chat.sh" to the correct script name
"test-inference-local-chat.sh" so the example reflects the actual file; locate
the top-of-file usage block containing the string "bash
demo-inference-local-chat.sh" and replace it with "bash
test-inference-local-chat.sh".
test/e2e-gateway-isolation.sh-37-48 (1)

37-48: ⚠️ Potential issue | 🟡 Minor

Don't reuse the default image tag for local runs.

When NEMOCLAW_TEST_IMAGE is unset, this still skips the build if nemoclaw-isolation-test already exists locally. A failed or interrupted previous run can then make this script validate an old image instead of the current checkout.

♻️ Suggested guard
-# Skip build if image already exists (e.g., loaded from CI artifact)
-if docker image inspect "$IMAGE" >/dev/null 2>&1; then
+# Only reuse a preloaded image when the caller explicitly provided one.
+if [ -n "${NEMOCLAW_TEST_IMAGE:-}" ] && docker image inspect "$IMAGE" >/dev/null 2>&1; then
   info "Using pre-built image: $IMAGE"
 else
   info "Building sandbox image..."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e-gateway-isolation.sh` around lines 37 - 48, The script currently
reuses a potentially stale local image when NEMOCLAW_TEST_IMAGE is unset because
IMAGE defaults to a fixed tag; change the logic so that when NEMOCLAW_TEST_IMAGE
is empty you generate a unique tag (e.g., include git commit SHA or a
timestamp/short UUID) and assign it to IMAGE before the docker image inspect
check, or alternatively skip the inspect check and always run the docker build
when NEMOCLAW_TEST_IMAGE is unset; update the code paths around the IMAGE
variable and the docker build block so the build is forced for local runs unless
an explicit NEMOCLAW_TEST_IMAGE is provided.
🧹 Nitpick comments (16)
.github/workflows/docs-preview-deploy.yaml (1)

100-106: Optional: gate sticky-comment deletion to same-repo PRs.

This avoids unnecessary API calls on fork PR closures, since preview comments are only created for same-repo PRs.

Suggested diff
-      - name: Remove preview comment
-        if: steps.meta.outputs.action == 'closed'
+      - name: Remove preview comment
+        if: steps.meta.outputs.action == 'closed' && steps.meta.outputs.same-repo == 'true'
         uses: marocchino/sticky-pull-request-comment@v2
         with:
           header: pr-preview
           number: ${{ steps.meta.outputs.pr-number }}
           delete: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docs-preview-deploy.yaml around lines 100 - 106, Update
the "Remove preview comment" step so it only runs for closed PRs that originate
from the same repository; change the step's if condition (currently if:
steps.meta.outputs.action == 'closed') to also verify the PR is not from a fork
by comparing the PR head repo to the workflow repo (for example: ensure
github.event.pull_request.head.repo.full_name == github.repository in the if
expression) so the marocchino/sticky-pull-request-comment deletion only executes
for same-repo PRs.
scripts/docs-to-skills.py (1)

1350-1373: Fragile path matching using string containment.

The check if ".agents/skills" in str(out_dir) can match unintended paths like .agents/skills-backup/, .agents/skills2/, or subdirectories like .agents/skills/foo/. Consider using a more precise check.

♻️ Proposed fix for precise path matching
             # Only create symlink if output is under .agents/skills
-            if ".agents/skills" in str(out_dir):
-                agents_skills = Path(out_dir)
+            agents_skills = Path(".agents/skills")
+            try:
+                # Check if out_dir is exactly .agents/skills or a child of it
+                out_dir.resolve().relative_to(agents_skills.resolve())
+                is_agents_skills_output = True
+            except ValueError:
+                is_agents_skills_output = out_dir.resolve() == agents_skills.resolve()
+            if is_agents_skills_output:

Alternatively, for an exact match only:

if out_dir.resolve() == Path(".agents/skills").resolve():
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/docs-to-skills.py` around lines 1350 - 1373, The current fragile
check uses string containment ("if '.agents/skills' in str(out_dir)") which can
match unintended paths; update the logic around args.output_dirs and out_dir to
perform a precise Path comparison instead (e.g., compare out_dir.resolve() to
Path(".agents/skills").resolve() or test
out_dir.samefile(Path(".agents/skills"))), locating the check in the loop that
iterates args.output_dirs and references claude_skills, agents_skills, and rel;
replace the substring match with the exact Path equality/samefile check so only
the intended .agents/skills directory triggers symlink creation.
docs/_ext/search_assets/modules/SearchEngine.js (1)

235-237: Consider logging skipped documents during index build.

At Line 235, swallowing per-document indexing failures without any trace makes data-quality regressions hard to detect.

Proposed refactor
-                } catch (_docError) {
-                    // Skip documents that fail to index
+                } catch (docError) {
+                    console.warn('Skipping document during index build', {
+                        docId: doc?.id,
+                        error: docError
+                    });
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/_ext/search_assets/modules/SearchEngine.js` around lines 235 - 237, The
catch block currently swallows per-document indexing errors (catch (_docError))
so failures are invisible; update that catch to log the error and document
identifier/metadata (e.g., doc.id or the document object) using the module's
logger if available (this.logger or similar) or console.error, including a clear
message and _docError details, then continue to skip the document as before.
nemoclaw/src/onboard/config.ts (2)

72-85: Consider improving error handling in the fallback path.

The fallback logic in ensureConfigDir catches all errors silently (line 77) and the fallback creation (lines 79-80) can also throw without being caught. If both paths fail, the error from the fallback will propagate with no context about the original failure.

♻️ Suggested improvement
 function ensureConfigDir(): void {
   if (configDirCreated) return;
   if (!existsSync(configDir)) {
     try {
       mkdirSync(configDir, { recursive: true });
-    } catch {
+    } catch (primaryError) {
       configDir = join(tmpdir(), ".nemoclaw");
       if (!existsSync(configDir)) {
-        mkdirSync(configDir, { recursive: true });
+        try {
+          mkdirSync(configDir, { recursive: true });
+        } catch (fallbackError) {
+          throw new Error(
+            `Failed to create config directory at primary (${process.env.HOME}/.nemoclaw) and fallback (${configDir}): ${String(fallbackError)}`
+          );
+        }
       }
     }
   }
   configDirCreated = true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/onboard/config.ts` around lines 72 - 85, The ensureConfigDir
function currently swallows the original error in the first mkdirSync attempt
and does not guard the fallback mkdirSync, so failures lose context; update
ensureConfigDir to catch the initial error into a variable (e.g., err1) when the
first mkdirSync fails, then attempt the fallback (set configDir = join(tmpdir(),
".nemoclaw")) inside its own try/catch so you can capture a second error (e.g.,
err2) if that also fails, and finally throw a new Error that includes both err1
and err2 messages (or attach them as cause) to preserve full context; reference
ensureConfigDir, configDir, configDirCreated, mkdirSync, existsSync, tmpdir, and
join when making the changes.

8-8: Module-level mutable state may cause test isolation issues.

The let configDir is mutable and modified by ensureConfigDir(). Once the fallback path is used, subsequent calls in the same process will use the modified path. This could cause unexpected behavior in tests or long-running processes.

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

In `@nemoclaw/src/onboard/config.ts` at line 8, The module-level mutable variable
configDir causes shared-state leaks; replace it with a pure accessor and stop
mutating module state: remove the let configDir and add a function
getConfigDir() that returns join(process.env.HOME ?? tmpdir(), ".nemoclaw"),
update ensureConfigDir() to call getConfigDir() (or accept an optional path
param) and remove any code that reassigns configDir, and update all other
references to use getConfigDir() so tests and long-running processes no longer
rely on mutated module state.
package.json (1)

14-14: Consider extracting the complex prepare script to a separate shell script.

The inline prepare script handles multiple conditional paths (git presence, prek binary, prek package). While functional, extracting this to a dedicated scripts/prepare.sh would improve readability and maintainability.

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

In `@package.json` at line 14, The inline "prepare" entry in package.json is doing
multiple conditional checks and should be moved to a dedicated script for
clarity and maintainability: create a new executable scripts/prepare.sh that
contains the current conditional logic (checking for .git, testing for the prek
binary, and handling the presence of node_modules/@j178/prek), make it
executable, and then replace the package.json "prepare" value with a simple
invocation of that script (e.g., calling scripts/prepare.sh). Ensure the new
script preserves identical behavior and exit codes and reference the same
symbols (prek, node_modules/@j178/prek) used in the original prepare logic.
test/e2e/e2e-cloud-experimental/skip/04-nemoclaw-openshell-status-parity.sh (1)

52-65: Consider simplifying the Ready check condition.

Line 61 checks cols.includes("Ready") && !cols.includes("NotReady"). If columns include "Ready" as a status, the !cols.includes("NotReady") check may be unnecessary unless a row can contain both states simultaneously. This is minor and the current implementation is safe.

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

In `@test/e2e/e2e-cloud-experimental/skip/04-nemoclaw-openshell-status-parity.sh`
around lines 52 - 65, The Ready-check inside the Node one-liner can be
simplified by removing the redundant "!cols.includes('NotReady')" check; update
the predicate that sets ok (the arrow function used in
clean.split(...).some(...)) to only verify cols.includes("Ready") (or a
word-boundary check like matching /\bReady\b/ against the trimmed line) so the
readiness determination uses a single clear condition; modify the lambda that
assigns ok accordingly.
test/credentials.test.js (1)

33-42: Prefer behavior assertions over source-regex assertions.

At Line 39–Line 41, this test is tightly coupled to code formatting/order and can fail on harmless refactors. Consider exercising the prompt error path and asserting observable behavior (rejection + SIGINT intent) instead of matching source text.

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

In `@test/credentials.test.js` around lines 33 - 42, The test currently inspects
credentials.js source via regex (matching "return new Promise((resolve, reject)
=> {" and 'reject(err); process.kill(process.pid, "SIGINT");'), which is
fragile; instead, mock/stub the secret prompt used by the credentials.js module
so it throws an error, spy on process.kill, import and invoke the exported
function that triggers the secret prompt, then assert the returned promise
rejects and that process.kill was called with process.pid and "SIGINT" (replace
the source-regex assertions with these behavioral assertions).
scripts/check-spdx-headers.sh (2)

38-46: Consider adding .jsx to the // comment style pattern.

The pattern handles .tsx but omits .jsx, which should also use // style comments.

♻️ Suggested fix
   case "$base" in
     Dockerfile | *.dockerfile | *.Dockerfile) echo "#" ;;
-    *.ts | *.tsx | *.js | *.mjs | *.cjs) echo "//" ;;
+    *.ts | *.tsx | *.js | *.jsx | *.mjs | *.cjs) echo "//" ;;
     *) echo "#" ;;
   esac
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-spdx-headers.sh` around lines 38 - 46, The case in function
comment_style_for currently maps *.ts, *.tsx, *.js, *.mjs, *.cjs to "//" but
omits JSX files; update the pattern in comment_style_for to include *.jsx
alongside *.tsx so files like *.jsx also return "//". Locate the case arm inside
comment_style_for and add "*.jsx" to the list of alternatives that echo "//"
(keeping the existing order and spacing).

61-85: Minor: temp file not cleaned up on partial failure in insert_spdx.

If chmod or mv fails on Line 84, the temp file in $tmp is left behind. Consider adding cleanup logic or a trap within the function.

♻️ Suggested improvement
 insert_spdx() {
   local file=$1
   local style
   style=$(comment_style_for "$file")
   local tmp
   local mode
   tmp="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-spdx.XXXXXX")"
+  trap 'rm -f "$tmp"' RETURN
   mode="$(stat -c '%a' "$file" 2>/dev/null || stat -f '%Lp' "$file")"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-spdx-headers.sh` around lines 61 - 85, The insert_spdx function
can leave the temporary file ($tmp) behind if chmod or mv fails; update
insert_spdx to create a cleanup trap or explicit error-path removal that deletes
"$tmp" on any failure and disables the trap after successful mv, ensuring we
still preserve the original file mode (mode variable) and that the trap
references the same tmp variable; target the tmp creation/cleanup around the
mktemp, chmod and mv sequence inside insert_spdx and ensure the trap is cleaned
up on success.
test/validate-blueprint.test.ts (2)

22-28: Consider adding error handling for blueprint parsing.

Parsing blueprint.yaml at module scope means any parsing errors will crash the entire test suite rather than reporting as individual test failures. Consider wrapping the parsing in a try-catch or moving it inside a beforeAll hook.

♻️ Suggested improvement
-const bp = YAML.parse(readFileSync(BLUEPRINT_PATH, "utf-8")) as Record<string, unknown>;
-const declared = Array.isArray(bp?.profiles) ? (bp.profiles as string[]) : [];
-const defined =
-  (bp?.components as Record<string, unknown> | undefined)?.inference != null
-    ? ((bp.components as Record<string, unknown>).inference as Record<string, unknown>).profiles as
-        Record<string, Record<string, unknown>> | undefined
-    : undefined;
+let bp: Record<string, unknown>;
+let declared: string[];
+let defined: Record<string, Record<string, unknown>> | undefined;
+
+beforeAll(() => {
+  bp = YAML.parse(readFileSync(BLUEPRINT_PATH, "utf-8")) as Record<string, unknown>;
+  declared = Array.isArray(bp?.profiles) ? (bp.profiles as string[]) : [];
+  defined =
+    (bp?.components as Record<string, unknown> | undefined)?.inference != null
+      ? ((bp.components as Record<string, unknown>).inference as Record<string, unknown>).profiles as
+          Record<string, Record<string, unknown>> | undefined
+      : undefined;
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/validate-blueprint.test.ts` around lines 22 - 28, Parsing blueprint.yaml
at module scope (YAML.parse(readFileSync(BLUEPRINT_PATH, "utf-8")) producing bp
and derived declared/defined) can crash the whole test run on parse errors; move
the file read and YAML.parse into a beforeAll hook or wrap them in a try-catch
inside beforeAll so you can surface parsing errors as test failures (use
BLUEPRINT_PATH, readFileSync, YAML.parse, bp, declared, defined) and call fail()
or throw a controlled error from the hook to report the failure without aborting
the test runner.

51-60: Clarify endpoint validation logic for dynamic_endpoint profiles.

When dynamic_endpoint === true, the test only verifies the endpoint key exists (Line 56) but doesn't check its value. When false, it requires a truthy value (Line 58). This asymmetry is intentional per the summary but could benefit from a brief comment explaining why dynamic endpoints allow empty/placeholder values.

📝 Suggested clarification
       for (const field of REQUIRED_PROFILE_FIELDS) {
         it(`has non-empty '${field}'`, () => {
           const cfg = defined?.[name];
           if (!cfg) return; // covered by "has a definition"
+          // dynamic_endpoint profiles may have placeholder endpoint values resolved at runtime
           if (field === "endpoint" && cfg.dynamic_endpoint === true) {
             expect(field in cfg).toBe(true);
           } else {
             expect(cfg[field]).toBeTruthy();
           }
         });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/validate-blueprint.test.ts` around lines 51 - 60, The test in the loop
over REQUIRED_PROFILE_FIELDS treats "endpoint" specially when
cfg.dynamic_endpoint === true by only asserting the key exists rather than
asserting a truthy value; add a short explanatory comment inside that branch
(near the conditional checking dynamic_endpoint and the expect(field in cfg)
line) clarifying that profiles with dynamic_endpoint may provide a
placeholder/empty endpoint at config time and are validated dynamically at
runtime, so the spec only requires the key to be present and not a truthy value;
reference REQUIRED_PROFILE_FIELDS, cfg.dynamic_endpoint, and the endpoint check
in the test to locate where to add the comment.
docs/reference/inference-profiles.md (2)

56-64: Colons used as general punctuation between clauses.

Per the style guide, colons should only introduce a list. Lines 56, 58, 60, and 62 use colons to separate a label from its explanation. Consider restructuring these items.

Suggested restructure
-- OpenAI-compatible providers:
-  NemoClaw tries `/responses` first, then `/chat/completions`.
+- **OpenAI-compatible providers** — NemoClaw tries `/responses` first, then `/chat/completions`.

Or use a definition list / nested structure where the provider type is a sub-heading.

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

In `@docs/reference/inference-profiles.md` around lines 56 - 64, The documentation
uses colons incorrectly as general punctuation in the list items describing
provider behaviors; update the sentences for the
OpenAI/Anthropic/NVIDIA/Compatible endpoint lines so the provider labels are not
followed by colons (e.g., rephrase "OpenAI-compatible providers: NemoClaw tries
`/responses` first..." to "For OpenAI-compatible providers, NemoClaw tries
`/responses` first, then `/chat/completions`") or convert the list into a
definition/nested structure with provider types as sub-headings (refer to the
terms "OpenAI-compatible providers", "Anthropic-compatible providers", "NVIDIA
Endpoints manual model entry", and "Compatible endpoint flows") so each label
introduces the explanation without using a colon as general punctuation.

86-88: Missing "Next Steps" section.

Per page structure guidelines, documentation pages should include a "Next Steps" section at the bottom linking to related pages. The current ending abruptly references another document without providing additional navigation options.

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

In `@docs/reference/inference-profiles.md` around lines 86 - 88, Add a "Next
Steps" section after the "Runtime Switching" paragraph that provides clear
navigation to related docs; include the existing "Switch Inference Models" link
(link text "Switch Inference Models") plus 1–2 other relevant pages such as
"Inference Providers" and "Model Selection" (or similar) to guide readers, using
the same relative-link style as the current reference to ensure consistent
formatting and placement at the bottom of the page.
test/onboard.test.js (1)

427-436: Consider using findLast for cleaner JSON payload extraction.

The .slice().reverse().find() pattern works but findLast (available in Node 18+) is more idiomatic.

Suggested simplification
-    const payloadLine = result.stdout
-      .trim()
-      .split("\n")
-      .slice()
-      .reverse()
-      .find((line) => line.startsWith("{") && line.endsWith("}"));
+    const payloadLine = result.stdout
+      .trim()
+      .split("\n")
+      .findLast((line) => line.startsWith("{") && line.endsWith("}"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/onboard.test.js` around lines 427 - 436, Replace the verbose
reverse-find pattern used to extract the JSON payload from stdout (the
payloadLine computation that uses
result.stdout.trim().split("\n").slice().reverse().find(...)) with the more
idiomatic Node 18+ Array.prototype.findLast call to locate the last line that
starts with "{" and ends with "}", keeping the subsequent
JSON.parse(payloadLine) and assertions (payload, payload.liveExists,
payload.sandbox) unchanged.
.agents/skills/nemoclaw-reference/references/inference-profiles.md (1)

51-53: Consider varying the list item phrasing.

Static analysis flagged three successive items beginning with "Local". While not incorrect, varying the structure improves readability.

Suggested alternative
-- Local Ollama
-- Local NVIDIA NIM
-- Local vLLM
+- Ollama (local)
+- NVIDIA NIM (local)
+- vLLM (local)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/nemoclaw-reference/references/inference-profiles.md around
lines 51 - 53, The three list items ("Local Ollama", "Local NVIDIA NIM", "Local
vLLM") use the same leading word which hurts readability; change the phrasing to
vary structure (for example: "Ollama (local)", "NVIDIA NIM — local deployment",
"vLLM (local runtime)") or similar alternations so each item reads distinctively
while preserving meaning; update the items where these exact strings appear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec12dd33-e5be-4c15-b2fc-e5c4254bf7c0

📥 Commits

Reviewing files that changed from the base of the PR and between 62866a3 and a0bc8ca.

⛔ Files ignored due to path filters (3)
  • nemoclaw-blueprint/uv.lock is excluded by !**/*.lock
  • nemoclaw/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (132)
  • .agents/skills/docs/nemoclaw-configure-inference/SKILL.md
  • .agents/skills/docs/nemoclaw-reference/references/inference-profiles.md
  • .agents/skills/nemoclaw-configure-inference/SKILL.md
  • .agents/skills/nemoclaw-deploy-remote/SKILL.md
  • .agents/skills/nemoclaw-get-started/SKILL.md
  • .agents/skills/nemoclaw-manage-policy/SKILL.md
  • .agents/skills/nemoclaw-monitor-sandbox/SKILL.md
  • .agents/skills/nemoclaw-overview/SKILL.md
  • .agents/skills/nemoclaw-overview/references/how-it-works.md
  • .agents/skills/nemoclaw-overview/references/overview.md
  • .agents/skills/nemoclaw-overview/references/release-notes.md
  • .agents/skills/nemoclaw-reference/SKILL.md
  • .agents/skills/nemoclaw-reference/references/architecture.md
  • .agents/skills/nemoclaw-reference/references/commands.md
  • .agents/skills/nemoclaw-reference/references/inference-profiles.md
  • .agents/skills/nemoclaw-reference/references/network-policies.md
  • .agents/skills/nemoclaw-reference/references/troubleshooting.md
  • .agents/skills/nemoclaw-workspace/SKILL.md
  • .agents/skills/nemoclaw-workspace/references/workspace-files.md
  • .claude/skills
  • .dockerignore
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/workflows/docs-preview-deploy.yaml
  • .github/workflows/e2e-brev.yaml
  • .github/workflows/nightly-e2e.yaml
  • .github/workflows/pr.yaml
  • .gitignore
  • .pre-commit-config.yaml
  • Dockerfile
  • Makefile
  • README.md
  • bin/lib/credentials.js
  • bin/lib/inference-config.js
  • bin/lib/local-inference.js
  • bin/lib/nim.js
  • bin/lib/onboard.js
  • bin/lib/policies.js
  • bin/lib/preflight.js
  • bin/lib/registry.js
  • bin/lib/resolve-openshell.js
  • bin/nemoclaw.js
  • ci/coverage-threshold.json
  • commitlint.config.js
  • docs/_ext/search_assets/main.js
  • docs/_ext/search_assets/modules/ResultRenderer.js
  • docs/_ext/search_assets/modules/SearchEngine.js
  • docs/_ext/search_assets/modules/SearchPageManager.js
  • docs/about/how-it-works.md
  • docs/inference/switch-inference-providers.md
  • docs/reference/architecture.md
  • docs/reference/commands.md
  • docs/reference/inference-profiles.md
  • docs/reference/troubleshooting.md
  • eslint.config.mjs
  • install.sh
  • jsconfig.json
  • nemoclaw-blueprint/Makefile
  • nemoclaw-blueprint/migrations/snapshot.py
  • nemoclaw-blueprint/orchestrator/__init__.py
  • nemoclaw-blueprint/orchestrator/runner.py
  • nemoclaw-blueprint/orchestrator/test_endpoint_validation.py
  • nemoclaw-blueprint/policies/openclaw-sandbox.yaml
  • nemoclaw-blueprint/policies/presets/discord.yaml
  • nemoclaw-blueprint/policies/presets/docker.yaml
  • nemoclaw-blueprint/policies/presets/huggingface.yaml
  • nemoclaw-blueprint/policies/presets/jira.yaml
  • nemoclaw-blueprint/policies/presets/outlook.yaml
  • nemoclaw-blueprint/policies/presets/slack.yaml
  • nemoclaw-blueprint/policies/presets/telegram.yaml
  • nemoclaw-blueprint/pyproject.toml
  • nemoclaw/eslint.config.mjs
  • nemoclaw/package.json
  • nemoclaw/src/blueprint/runner.test.ts
  • nemoclaw/src/blueprint/runner.ts
  • nemoclaw/src/blueprint/snapshot.test.ts
  • nemoclaw/src/blueprint/snapshot.ts
  • nemoclaw/src/blueprint/ssrf.test.ts
  • nemoclaw/src/blueprint/ssrf.ts
  • nemoclaw/src/blueprint/state.ts
  • nemoclaw/src/commands/migration-state.test.ts
  • nemoclaw/src/index.ts
  • nemoclaw/src/onboard/config.test.ts
  • nemoclaw/src/onboard/config.ts
  • package.json
  • scripts/backup-workspace.sh
  • scripts/check-spdx-headers.sh
  • scripts/docs-to-skills.py
  • scripts/nemoclaw-start.sh
  • scripts/setup-spark.sh
  • scripts/setup.sh
  • scripts/telegram-bridge.js
  • scripts/walkthrough.sh
  • scripts/write-auth-profile.py
  • scripts/write-auth-profile.ts
  • spark-install.md
  • test/Dockerfile.sandbox
  • test/credential-exposure.test.js
  • test/credentials.test.js
  • test/e2e-gateway-isolation.sh
  • test/e2e-test.sh
  • test/e2e/brev-e2e.test.js
  • test/e2e/e2e-cloud-experimental/check-docs.sh
  • test/e2e/e2e-cloud-experimental/checks/02-inference-local-http.sh
  • test/e2e/e2e-cloud-experimental/checks/03-security-checks.sh
  • test/e2e/e2e-cloud-experimental/expect-interactive-install.sh
  • test/e2e/e2e-cloud-experimental/features/skill/add-sandbox-skill.sh
  • test/e2e/e2e-cloud-experimental/features/skill/fixtures/skill-smoke-template.SKILL.md
  • test/e2e/e2e-cloud-experimental/features/skill/lib/README.md
  • test/e2e/e2e-cloud-experimental/features/skill/lib/validate_repo_skills.sh
  • test/e2e/e2e-cloud-experimental/features/skill/lib/validate_sandbox_openclaw_skills.sh
  • test/e2e/e2e-cloud-experimental/features/skill/verify-sandbox-skill-via-agent.sh
  • test/e2e/e2e-cloud-experimental/openclaw-tui-in-sandbox.sh
  • test/e2e/e2e-cloud-experimental/skip/01-onboard-completion.sh
  • test/e2e/e2e-cloud-experimental/skip/04-nemoclaw-openshell-status-parity.sh
  • test/e2e/e2e-cloud-experimental/skip/05-network-policy.sh
  • test/e2e/e2e-cloud-experimental/skip/README.md
  • test/e2e/e2e-cloud-experimental/test-inference-local-chat.sh
  • test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh
  • test/e2e/test-double-onboard.sh
  • test/e2e/test-e2e-cloud-experimental.sh
  • test/e2e/test-full-e2e.sh
  • test/inference-config.test.js
  • test/local-inference.test.js
  • test/nemoclaw-start.test.js
  • test/onboard-selection.test.js
  • test/onboard.test.js
  • test/policies.test.js
  • test/preflight.test.js
  • test/runner.test.js
  • test/security-binaries-restriction.test.js
  • test/validate-blueprint.test.ts
  • vitest.config.ts
💤 Files with no reviewable changes (10)
  • .dockerignore
  • nemoclaw-blueprint/orchestrator/init.py
  • nemoclaw-blueprint/pyproject.toml
  • .agents/skills/docs/nemoclaw-configure-inference/SKILL.md
  • .agents/skills/docs/nemoclaw-reference/references/inference-profiles.md
  • scripts/write-auth-profile.py
  • nemoclaw-blueprint/orchestrator/runner.py
  • nemoclaw-blueprint/Makefile
  • nemoclaw-blueprint/orchestrator/test_endpoint_validation.py
  • nemoclaw-blueprint/migrations/snapshot.py
✅ Files skipped from review due to trivial changes (31)
  • .agents/skills/nemoclaw-reference/references/troubleshooting.md
  • commitlint.config.js
  • .gitignore
  • .agents/skills/nemoclaw-get-started/SKILL.md
  • .claude/skills
  • .github/PULL_REQUEST_TEMPLATE.md
  • bin/lib/registry.js
  • nemoclaw-blueprint/policies/presets/huggingface.yaml
  • .agents/skills/nemoclaw-reference/SKILL.md
  • bin/lib/resolve-openshell.js
  • docs/reference/architecture.md
  • docs/reference/troubleshooting.md
  • .agents/skills/nemoclaw-reference/references/commands.md
  • ci/coverage-threshold.json
  • docs/_ext/search_assets/modules/ResultRenderer.js
  • nemoclaw/src/index.ts
  • docs/reference/commands.md
  • scripts/setup-spark.sh
  • scripts/telegram-bridge.js
  • docs/_ext/search_assets/main.js
  • .agents/skills/nemoclaw-workspace/references/workspace-files.md
  • nemoclaw/src/commands/migration-state.test.ts
  • docs/inference/switch-inference-providers.md
  • jsconfig.json
  • test/e2e/e2e-cloud-experimental/features/skill/lib/README.md
  • test/e2e/e2e-cloud-experimental/skip/README.md
  • .agents/skills/nemoclaw-overview/references/how-it-works.md
  • test/e2e/e2e-cloud-experimental/features/skill/fixtures/skill-smoke-template.SKILL.md
  • test/e2e/e2e-cloud-experimental/expect-interactive-install.sh
  • .agents/skills/nemoclaw-configure-inference/SKILL.md
  • .agents/skills/nemoclaw-workspace/SKILL.md

Comment thread bin/lib/onboard.js
Comment on lines +1415 to 1424
// Only pass non-sensitive env vars to the sandbox. NVIDIA_API_KEY is NOT
// needed inside the sandbox — inference is proxied through the OpenShell
// gateway which injects the stored credential server-side. The gateway
// also strips any Authorization headers sent by the sandbox client.
// See: crates/openshell-sandbox/src/proxy.rs (header stripping),
// crates/openshell-router/src/backend.rs (server-side auth injection).
const envArgs = [formatEnvAssignment("CHAT_UI_URL", chatUiUrl)];
const sandboxEnv = { ...process.env };
delete sandboxEnv.NVIDIA_API_KEY;
const discordToken = getCredential("DISCORD_BOT_TOKEN") || process.env.DISCORD_BOT_TOKEN;

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.

⚠️ Potential issue | 🟠 Major

The sandbox env still includes the new remote-provider secrets.

sandboxEnv clones process.env and only deletes NVIDIA_API_KEY. After adding OpenAI, Anthropic, Gemini, and custom endpoint flows, those API keys will still be inherited by openshell sandbox create, which contradicts the “non-sensitive only” contract in this block.

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

In `@bin/lib/onboard.js` around lines 1415 - 1424, The sandboxEnv currently clones
process.env then only deletes NVIDIA_API_KEY, which still leaks remote-provider
secrets (OpenAI/Anthropic/Gemini/custom endpoints). Update the logic around
sandboxEnv (the object created near formatEnvAssignment and getCredential usage)
to remove or avoid copying these provider secrets: at minimum delete
OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY and any custom provider
keys/endpoints your app uses (e.g., REMOTE_PROVIDER_API_KEY or
REMOTE_PROVIDER_ENDPOINT) from sandboxEnv before passing to the sandbox;
alternatively replace the clone-with-deletes with a whitelist approach (build
sandboxEnv only from safe keys such as CHAT_UI_URL and explicit non-sensitive
vars). Ensure getCredential usage for DISCORD_BOT_TOKEN remains unchanged.

Comment thread bin/lib/onboard.js
Comment on lines +1783 to +1788
preferredInferenceApi = await validateOpenAiLikeSelection(
"Local NVIDIA NIM",
endpointUrl,
model,
credentialEnv
);

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.

⚠️ Potential issue | 🟠 Major

Don't send the user's OpenAI key to local NIM/vLLM probes.

These calls pass credentialEnv = "OPENAI_API_KEY" into validateOpenAiLikeSelection(), so a saved cloud key gets attached as a Bearer token when probing localhost. Local NIM/vLLM do not need that secret, and this leaks it into local service logs unnecessarily.

Also applies to: 1899-1904

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

In `@bin/lib/onboard.js` around lines 1783 - 1788, The code is passing the cloud
credentialEnv ("OPENAI_API_KEY") into validateOpenAiLikeSelection for local
backends, which causes the user's OpenAI key to be sent to local probes; update
the calls that construct preferredInferenceApi for local options (e.g. the
invocation with "Local NVIDIA NIM" and the similar "Local vLLM" call) to pass no
credentialEnv (use null/undefined or an explicit empty string) so
validateOpenAiLikeSelection does not attach a Bearer token when probing
localhost, and ensure validateOpenAiLikeSelection still handles a missing
credentialEnv safely.

Comment on lines +264 to +289
const credentialEnv = inferenceCfg.credential_env;
const credentialDefault = inferenceCfg.credential_default ?? "";
let credential = "";
if (credentialEnv) {
credential = process.env[credentialEnv] ?? credentialDefault;
}

const providerArgs = [
"openshell",
"provider",
"create",
"--name",
providerName,
"--type",
providerType,
];
// Pass the env-var NAME (not the value) to --credential; openshell reads the value from the env.
// Scope the credential to the subprocess to avoid leaking into later commands.
const credEnv: Record<string, string> = {};
if (credential) {
credEnv.OPENAI_API_KEY = credential;
providerArgs.push("--credential", "OPENAI_API_KEY");
}
if (endpoint) {
providerArgs.push("--config", `OPENAI_BASE_URL=${endpoint}`);
}

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.

⚠️ Potential issue | 🟠 Major

Map credentials and base-url config by provider_type.

actionApply() always rewrites the secret into OPENAI_API_KEY and always sends OPENAI_BASE_URL=.... That only matches OpenAI-style providers; Anthropic and any other provider types modeled in the blueprint will be configured with the wrong env/config keys.

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

In `@nemoclaw/src/blueprint/runner.ts` around lines 264 - 289, The code currently
hardcodes OPENAI_* env/config keys when setting credentials and base URL; change
this to map keys by providerType (use the existing providerType/inferenceCfg
variables). Create a small map or function (e.g., const providerKeyMap = {
openai: {cred: "OPENAI_API_KEY", base: "OPENAI_BASE_URL"}, anthropic: {cred:
"ANTHROPIC_API_KEY", base: "ANTHROPIC_API_URL"}, ... }) and use it inside the
credential/endpoint branch where providerArgs, credEnv, and --config are built:
set const credVar = providerKeyMap[providerType].cred (fallback to a safe
default), assign credEnv[credVar] = credential and push ("--credential",
credVar), and for endpoint push ("--config",
`${providerKeyMap[providerType].base}=${endpoint}`). Update any related use in
actionApply() to use the same mapping so non-OpenAI providers get the correct
env/config keys.

Comment on lines +291 to +301
await execa(providerArgs[0], providerArgs.slice(1), {
reject: false,
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, ...credEnv },
});

progress(70, "Setting inference route");
await runCmd(["openshell", "inference", "set", "--provider", providerName, "--model", model], {
reject: false,
});

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.

⚠️ Potential issue | 🟠 Major

Treat provider setup and route selection failures as fatal.

Both subprocess results are discarded here. A failed provider create or inference set still falls through to PROGRESS:100 and writes plan.json as if apply succeeded, so reruns can silently keep stale routing.

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

In `@nemoclaw/src/blueprint/runner.ts` around lines 291 - 301, The subprocess
results from the execa call (using providerArgs) and the
runCmd(["openshell","inference","set",...]) call are currently ignored so
failures still allow progress to 100 and writing plan.json; capture their
results and treat non-zero exits as fatal: inspect the return (e.g.
result.exitCode or result.status) from the execa(...) invocation and from
runCmd(...) and if non-zero throw an Error (or process.exit(1)) including
stdout/stderr and context (providerName, model) to stop execution and avoid
calling progress(100) / writing plan.json; alternatively enable reject:true so
exceptions propagate and catch/log them with details before exiting.

Comment on lines +428 to +445
const blueprint = loadBlueprint();

switch (action) {
case "plan":
await actionPlan(profile, blueprint, { dryRun, endpointUrl });
break;
case "apply":
await actionApply(profile, blueprint, { planPath, endpointUrl });
break;
case "status":
actionStatus(runId);
break;
case "rollback":
if (!runId) {
throw new Error("--run-id is required for rollback");
}
await actionRollback(runId);
break;

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.

⚠️ Potential issue | 🟠 Major

Don't require blueprint.yaml for status and rollback.

main() loads the blueprint before dispatching any action, even though these two commands only use persisted run state. That makes routine inspection and rollback fail outside the original blueprint directory for no functional reason.

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

In `@nemoclaw/src/blueprint/runner.ts` around lines 428 - 445, Currently
loadBlueprint() is called unconditionally before the switch, causing status and
rollback to fail when blueprint.yaml is absent; change the flow so
loadBlueprint() is only invoked inside the cases that need it (call
loadBlueprint() inside the "plan" and "apply" branches before
actionPlan/actionApply) and remove the unconditional call so actionStatus(runId)
and actionRollback(runId) run without requiring a blueprint; ensure you keep the
existing checks (e.g., the --run-id check before actionRollback) and keep
function names exactly as in the diff (loadBlueprint, actionPlan, actionApply,
actionStatus, actionRollback).

Comment on lines +51 to +58
run_as_sandbox() {
docker run --rm --entrypoint "" "$IMAGE" gosu sandbox bash -c "$1" 2>&1
}

# Helper: run a command inside the container as root
run_as_root() {
docker run --rm --entrypoint "" "$IMAGE" bash -c "$1" 2>&1
}

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.

⚠️ Potential issue | 🟠 Major

These probes short-circuit the whole script under set -e.

OUT=$(run_as_root ...) / OUT=$(run_as_sandbox ...) inherits the inner exit code. For negative checks like missing users, missing gosu, missing iptables, or broken symlinks, Bash exits before fail() runs, so you lose the per-assertion summary.

🧪 Capture the exit status explicitly
-OUT=$(run_as_root "command -v gosu && gosu --version")
-if echo "$OUT" | grep -q "gosu"; then
+set +e
+OUT=$(run_as_root "command -v gosu && gosu --version")
+rc=$?
+set -e
+if [ "$rc" -eq 0 ] && echo "$OUT" | grep -q "gosu"; then
   pass "gosu installed"
 else
   fail "gosu not found: $OUT"
 fi

Also applies to: 63-65, 118-123, 139-143, 151-153, 165-169

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

In `@test/e2e-gateway-isolation.sh` around lines 51 - 58, The test calls that run
container commands via run_as_root() and run_as_sandbox() must not let their
inner exit code trigger the script-wide set -e; wrap each invocation so you
capture both stdout and the exit status (e.g. temporarily disable errexit with
set +e before running, capture OUT="$(run_as_root ...)" and rc=$?, then restore
set -e and use rc to decide pass/fail) or immediately follow the command with
rc=$? before any other command; update every occurrence that does
OUT=$(run_as_root ...) or OUT=$(run_as_sandbox ...) (and similar checks for
missing gosu/iptables/symlinks) to use this pattern so fail() runs and
per-assertion summaries aren't skipped.

Comment thread test/e2e/brev-e2e.test.js
Comment on lines +39 to +45
function brev(...args) {
return execFileSync("brev", args, {
encoding: "utf-8",
timeout: 60_000,
stdio: ["pipe", "pipe", "pipe"],
}).trim();
}

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.

⚠️ Potential issue | 🟠 Major

Isolate Brev state under a temp home for this suite.

This writes onboarding_step.json into the caller's real ~/.brev and then logs in against the same host config. A local run will mutate or overwrite an existing Brev session, and the suite never restores that state. Point the brev() helper and this file write at a temporary HOME/config directory and clean it up in afterAll.

Also applies to: 115-121

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

In `@test/e2e/brev-e2e.test.js` around lines 39 - 45, The test helper brev()
currently invokes the real user HOME and causes onboarding_step.json and session
files to be written into ~/.brev; change the suite to create a temporary
directory for HOME (or BREV_CONFIG_HOME) at test setup, update brev(...) to pass
env with HOME set to that temp dir so execFileSync isolates config, update any
direct writes that reference onboarding_step.json to target the temp config dir
instead (also apply the same change to the code around lines 115-121 that write
session/config), and add an afterAll cleanup that removes the temp directory and
restores any modified env.

Comment thread test/e2e/brev-e2e.test.js Outdated
Comment on lines +135 to +138
execSync(
`rsync -az --delete --exclude node_modules --exclude .git --exclude dist --exclude .venv "${REPO_DIR}/" "${INSTANCE_NAME}:${remoteDir}/"`,
{ encoding: "utf-8", timeout: 120_000 },
);

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.

⚠️ Potential issue | 🟠 Major

Don't rsync the whole checkout to the remote VM.

"${REPO_DIR}/" includes untracked local files, so this can upload .env*, .npmrc, ad-hoc keys, and other developer-only artifacts to the Brev instance. Prefer syncing tracked files only, or at minimum expand the exclude list to cover common secret files.

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

In `@test/e2e/brev-e2e.test.js` around lines 135 - 138, The rsync call using
"${REPO_DIR}/" copies untracked developer files (e.g. .env*, .npmrc, keys);
update the sync to only transfer tracked files or explicitly exclude common
secret patterns: replace the execSync rsync invocation that references REPO_DIR/
(the execSync call around variables REPO_DIR, INSTANCE_NAME, remoteDir) with one
of two options — either generate a list of tracked files via git (e.g. use `git
-C "${REPO_DIR}" ls-files -z` piped into rsync --from0 --files-from=- ...) so
only tracked files are synced, or expand the rsync --exclude list to include
sensitive filenames/patterns (.env*, .npmrc, *.pem, id_rsa*, *.key, .ssh/,
.netrc, etc.) to prevent uploading secrets.

OPENCLAW_AGENT_PREFIX="${OPENCLAW_AGENT_PREFIX:-nemoclaw-start}"
AGENT_LAUNCHER=""
[ -n "$OPENCLAW_AGENT_PREFIX" ] && AGENT_LAUNCHER="${OPENCLAW_AGENT_PREFIX} "
SESSION_ID="${SKILL_VERIFY_SESSION_ID:-skill-verify-$(date +%s)-${RANDOM}-${RANDOM}-${RANDOM}}"

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.

⚠️ Potential issue | 🟠 Major

Validate or escape SKILL_VERIFY_SESSION_ID before interpolating it into the remote shell.

SESSION_ID is copied straight into both the lock-file removal fragment and the final openclaw agent command. A caller-supplied value containing ', /, or shell metacharacters can break the command or point the cleanup at the wrong path. Mirror the SKILL_ID validation here or shell-escape it before building remote_cmd.

Suggested fix
 SESSION_ID="${SKILL_VERIFY_SESSION_ID:-skill-verify-$(date +%s)-${RANDOM}-${RANDOM}-${RANDOM}}"
+case "$SESSION_ID" in
+  *[!A-Za-z0-9._-]* | "") die "SKILL_VERIFY_SESSION_ID may only contain [A-Za-z0-9._-]" ;;
+esac

Also applies to: 62-66

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

In
`@test/e2e/e2e-cloud-experimental/features/skill/verify-sandbox-skill-via-agent.sh`
at line 30, SESSION_ID is set from SKILL_VERIFY_SESSION_ID and then interpolated
into remote_cmd and the lock-file removal, which is unsafe; mirror the SKILL_ID
validation used elsewhere (or explicitly shell-escape SKILL_VERIFY_SESSION_ID)
before assigning SESSION_ID so the value contains only allowed characters (e.g.,
alphanumerics, dashes, underscores) or is properly quoted/escaped, then use that
sanitized SESSION_ID in the lock-file removal fragment and in the openclaw agent
invocation to prevent injection or path confusion; update references to
SESSION_ID, remote_cmd, and any lock-file paths to use the sanitized value.

Comment thread vitest.config.ts
Comment on lines +22 to +29
{
test: {
name: "e2e-brev",
include: ["test/e2e/brev-e2e.test.js"],
// Only run when explicitly targeted: npx vitest run --project e2e-brev
enabled: !!process.env.BREV_API_TOKEN,
},
},

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Vitest project enabled option configuration

💡 Result:

In Vitest, there is no direct "enabled" option for projects to toggle them on/off in configuration. Projects are defined in the root test.projects array as globs, paths to config files, or inline objects. All defined projects run by default when executing vitest. To effectively "disable" a project: - Remove it from the projects array, or - Use CLI --project to run only specific projects by name (set via test.name), e.g., vitest --project unit --project e2e. Example configuration enabling multiple projects (from official docs): import { defineConfig } from 'vitest/config' export default defineConfig({ test: { projects: [ 'packages/', // Glob matching project directories { extends: true, test: { name: 'unit', include: ['/.unit.test.ts'] } } ] } }) Similar to browser.enabled: true for browser mode (requires provider setup), projects are enabled by inclusion in the array. Use include: [] or exclude: ['/*'] in a project config to skip test discovery if needed, though no explicit "enabled: false" exists. Run all: vitest Run specific: vitest --project unit

Citations:


Remove the invalid enabled property; Vitest does not support this configuration option for projects.

The enabled property at line 27 is not a valid Vitest project configuration option and will be silently ignored. Projects in Vitest are controlled by their presence in the test.projects array—all defined projects run by default. To conditionally run this project only when BREV_API_TOKEN is available, either:

  • Dynamically include/exclude this project from the test.projects array based on the environment variable, or
  • Remove it from configuration and use the CLI flag vitest --project e2e-brev to run it explicitly when needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` around lines 22 - 29, The project object that has name
"e2e-brev" contains an invalid property enabled which Vitest ignores; remove the
enabled property and instead conditionally include the project in the
test.projects array based on process.env.BREV_API_TOKEN (or remove the project
and run it via the CLI). Locate the test.projects array in vitest.config.ts and
either (a) wrap the "e2e-brev" project entry in a conditional that only adds it
when process.env.BREV_API_TOKEN is truthy, or (b) delete the "enabled" key and
keep the project static and rely on explicit CLI invocation (vitest --project
e2e-brev).

@wscurran wscurran added bug Something fails against expected or documented behavior Getting Started labels Mar 30, 2026
@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR with a detailed summary, it addresses a bug with the policy presets and proposes a fix to improve the user experience of NemoClaw, which could enhance the usability of the platform.

The policy presets step shows all presets with ○/● bullets, but
suggested presets (npm, pypi, auto-detected tokens) were displayed
with the same empty ○ bullet as non-suggested ones, making it unclear
which presets would be applied by default.

Include suggested presets in the filled ● bullet condition so they
appear visually pre-selected, matching the prompt behavior.

Fixes NVIDIA#706

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@temrjan temrjan force-pushed the fix/preselect-suggested-policy-presets branch from a0bc8ca to 0558e8f Compare March 31, 2026 02:22
@cv cv merged commit fb23f31 into NVIDIA:main Mar 31, 2026
9 of 12 checks passed
laitingsheng pushed a commit that referenced this pull request Apr 2, 2026
…777)

## Summary

Fixes #706

Suggested policy presets (e.g. `npm`, `pypi`) now display with a filled
`●` bullet instead of an empty `○`, matching the prompt behavior that
applies them by default.

## Details

**Before** (all presets show `○`):
```
○ npm — npm and Yarn registry access (suggested)
○ pypi — Python Package Index (PyPI) access (suggested)
```

**After** (suggested presets show `●`):
```
● npm — npm and Yarn registry access (suggested)
● pypi — Python Package Index (PyPI) access (suggested)
```

One-line change in `bin/lib/onboard.js`: include `suggestions` array in
the marker condition alongside `applied`.

## Test plan

- [x] Verified `npm test` — no new test failures
- [ ] Visual: run `nemoclaw onboard`, observe step 7 — suggested presets
should show `●`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Policy preset suggestions now display as selected in the interactive
listing before being officially applied. This change improves visual
feedback, making it clearer to users which presets are recommended for
their configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Temrjan <x.temrjan@gmail.com>

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lakamsani pushed a commit to lakamsani/NemoClaw that referenced this pull request Apr 4, 2026
…VIDIA#777)

## Summary

Fixes NVIDIA#706

Suggested policy presets (e.g. `npm`, `pypi`) now display with a filled
`●` bullet instead of an empty `○`, matching the prompt behavior that
applies them by default.

## Details

**Before** (all presets show `○`):
```
○ npm — npm and Yarn registry access (suggested)
○ pypi — Python Package Index (PyPI) access (suggested)
```

**After** (suggested presets show `●`):
```
● npm — npm and Yarn registry access (suggested)
● pypi — Python Package Index (PyPI) access (suggested)
```

One-line change in `bin/lib/onboard.js`: include `suggestions` array in
the marker condition alongside `applied`.

## Test plan

- [x] Verified `npm test` — no new test failures
- [ ] Visual: run `nemoclaw onboard`, observe step 7 — suggested presets
should show `●`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Policy preset suggestions now display as selected in the interactive
listing before being officially applied. This change improves visual
feedback, making it clearer to users which presets are recommended for
their configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Temrjan <x.temrjan@gmail.com>

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gemini2026 pushed a commit to gemini2026/NemoClaw that referenced this pull request Apr 14, 2026
…VIDIA#777)

## Summary

Fixes NVIDIA#706

Suggested policy presets (e.g. `npm`, `pypi`) now display with a filled
`●` bullet instead of an empty `○`, matching the prompt behavior that
applies them by default.

## Details

**Before** (all presets show `○`):
```
○ npm — npm and Yarn registry access (suggested)
○ pypi — Python Package Index (PyPI) access (suggested)
```

**After** (suggested presets show `●`):
```
● npm — npm and Yarn registry access (suggested)
● pypi — Python Package Index (PyPI) access (suggested)
```

One-line change in `bin/lib/onboard.js`: include `suggestions` array in
the marker condition alongside `applied`.

## Test plan

- [x] Verified `npm test` — no new test failures
- [ ] Visual: run `nemoclaw onboard`, observe step 7 — suggested presets
should show `●`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Policy preset suggestions now display as selected in the interactive
listing before being officially applied. This change improves visual
feedback, making it clearer to users which presets are recommended for
their configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Temrjan <x.temrjan@gmail.com>

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wscurran wscurran added area: install Install, setup, prerequisites, or uninstall flow area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow bug-fix PR fixes a bug or regression and removed Getting Started bug Something fails against expected or documented behavior labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: install Install, setup, prerequisites, or uninstall flow area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Ubuntu 24.04.3][Regression] Suggested policy presets are not visually pre-selected during installation

5 participants