Skip to content

fix(cli): add config-io module for safe config file I/O and preset validation#1370

Open
cv wants to merge 7 commits intomainfrom
cv/config-io-preset-validation
Open

fix(cli): add config-io module for safe config file I/O and preset validation#1370
cv wants to merge 7 commits intomainfrom
cv/config-io-preset-validation

Conversation

@cv
Copy link
Copy Markdown
Contributor

@cv cv commented Apr 2, 2026

Summary

  • New src/lib/config-io.ts: atomic JSON read/write (temp + rename), EACCES error handling with ConfigPermissionError and user-facing remediation hints
  • Refactor credentials.js to use readConfigFile/writeConfigFile (removes manual fs + chmod)
  • Refactor registry.js to use readConfigFile/writeConfigFile (removes manual atomic write)
  • Add validatePreset() to policies.js — warns when a preset lacks binaries: section (All policy presets and default telegram/discord policies return 403 due to missing binaries section #676)
  • Co-located tests for config-io (10 tests)

Supersedes the config-io and preset validation parts of #782 (without the runner.js redaction, which landed separately in #1246). Fixes #692, #606.

Test plan

  • 716 CLI tests pass
  • Coverage ratchet passes
  • Registry atomic write test updated to match new error format

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Safer config file I/O with clearer permission errors and user-facing remediation guidance.
  • Tests

    • Added comprehensive tests for config I/O covering atomic writes, permissions, defaults, error scenarios, and updated registry write-failure expectations.
  • Refactor

    • Centralized config persistence logic used by credential and registry handling for consistency.
  • Bug Fixes

    • Preset validation now warns when required sections are missing before applying presets.

…lidation

Add src/lib/config-io.ts with atomic JSON read/write (temp + rename),
EACCES error handling with user-facing remediation hints, and directory
permission enforcement.

- Refactor credentials.js to use readConfigFile/writeConfigFile
- Refactor registry.js to use readConfigFile/writeConfigFile
- Add validatePreset() to policies.js (warns on missing binaries section)
- ConfigPermissionError with actionable remediation (sudo chown / rm)
- Co-located tests for config-io module

Fixes #692, #606. Supersedes the config-io and preset validation parts
of #782 (without the runner.js redaction, which landed separately in
#1246).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Added a new permission-aware config I/O module with atomic write semantics and a permission error type; refactored registry and credentials to use it; added preset validation in policies; added tests and a CommonJS shim for the compiled config-io output.

Changes

Cohort / File(s) Summary
Config I/O Implementation
src/lib/config-io.ts, bin/lib/config-io.js
New TypeScript module providing ConfigPermissionError, ensureConfigDir(), writeConfigFile(), and readConfigFile() with atomic temp-file writes, EACCES-specific error wrapping; added CJS re-export shim in bin/lib/config-io.js.
Refactored File I/O Users
bin/lib/credentials.js, bin/lib/registry.js
Replaced inline fs/JSON handling and registry atomic-write/dir logic with calls to readConfigFile/writeConfigFile and ensureConfigDir from config-io; removed manual mkdir/chmod/atomic-write implementations.
Policy Validation
bin/lib/policies.js
Added and exported validatePreset(presetContent, presetName); applyPreset() calls it and returns early on invalid presets (warns when binaries: is missing).
Tests Added/Updated
src/lib/config-io.test.ts, test/registry.test.js
New Vitest suite for config-io covering dir perms (0o700), atomic writes, defaults for missing/corrupt files, temp-file cleanup, and ConfigPermissionError details; updated registry test to expect wrapped error message "Cannot write config file".

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant CIO as config-io
    participant FS as Filesystem

    App->>CIO: writeConfigFile(targetPath, data)
    CIO->>CIO: ensureConfigDir(parentDir)
    CIO->>FS: mkdir -p parentDir (mode 0o700)
    FS-->>CIO: success / EACCES
    alt EACCES
        CIO->>App: throw ConfigPermissionError (with remediation)
    else Success
        CIO->>FS: write temp file (0o600, PID-suffix)
        FS-->>CIO: write success / error
        CIO->>FS: rename(temp, target)
        FS-->>CIO: rename success / EACCES
        alt Rename EACCES
            CIO->>FS: unlink(temp) [best-effort]
            CIO->>App: throw ConfigPermissionError
        else Success
            CIO->>App: return
        end
    end
Loading
sequenceDiagram
    participant App as Application
    participant CIO as config-io
    participant FS as Filesystem

    App->>CIO: readConfigFile(path, default)
    CIO->>FS: stat/access path
    FS-->>CIO: exists / missing / EACCES
    alt EACCES
        CIO->>App: throw ConfigPermissionError
    else Missing
        CIO->>App: return default
    else Exists
        CIO->>FS: readFile(path)
        FS-->>CIO: contents
        CIO->>CIO: JSON.parse(contents)
        alt Parse error
            CIO->>App: return default
        else Success
            CIO->>App: return parsed data
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit's note on safe config:
I hop where files are written neat,
Temp names, strict modes, no messy sleet,
If perms deny, I sound the bell,
With remedies to fix and tell. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% 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 accurately summarizes the main changes: adding a config-io module for safe config file I/O and preset validation functionality.
Linked Issues check ✅ Passed The PR implements safe config file I/O with permission error handling and remediation guidance, addressing #692's core requirement for graceful EACCES handling and user-facing remediation.
Out of Scope Changes check ✅ Passed All changes are within scope: config-io module implementation, refactoring credentials/registry/policies to use it, and corresponding test updates directly support the linked issues.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cv/config-io-preset-validation

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@bin/lib/policies.js`:
- Around line 65-73: The current check in validatePreset using
presetContent.includes("binaries:") can be fooled by comments or scalars;
instead parse the YAML and detect a real top-level key. Replace the substring
check in validatePreset with a YAML parse (e.g., yaml.parse / js-yaml.safeLoad)
of presetContent and then check for Object.prototype.hasOwnProperty.call(parsed,
"binaries") (and that parsed.binaries is not undefined/null); if parsing fails,
log a warning and treat the preset as missing the binaries section so the
original warning is emitted for presetName.

In `@src/lib/config-io.ts`:
- Around line 29-41: The current buildRemediation() message assumes sudo is
available; update it to include non-sudo fallback actions so environments
without sudo get actionable guidance. In the buildRemediation() function
(reference: nemoclawDir and HOME usage) add alternative suggestions such as
recreating the config under the current user's home (e.g., remove or move the
directory if writable), instructing to remove the directory without sudo when
the user owns it (e.g., rm -rf $HOME/.nemoclaw), and advising to relocate or
initialize config in a user-writable path (for example creating a new config
under $HOME or specifying an alternative CONFIG_HOME), so the error message
covers both sudo and non-sudo environments. Ensure the text clearly
distinguishes when sudo is required vs when the non-sudo command applies.
🪄 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

Run ID: b236f71c-5208-4de3-9a9b-e2b0d59af3a1

📥 Commits

Reviewing files that changed from the base of the PR and between 86b3dee and 38424c6.

📒 Files selected for processing (7)
  • bin/lib/config-io.js
  • bin/lib/credentials.js
  • bin/lib/policies.js
  • bin/lib/registry.js
  • src/lib/config-io.test.ts
  • src/lib/config-io.ts
  • test/registry.test.js

Comment on lines +65 to +73
function validatePreset(presetContent, presetName) {
if (!presetContent.includes("binaries:")) {
console.warn(
` Warning: preset '${presetName}' has no binaries section — ` +
`this will cause 403 errors in the sandbox (ref: #676)`,
);
return false;
}
return true;
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 | 🟡 Minor

Detect the actual YAML key, not a raw substring.

presetContent.includes("binaries:") also matches comments, descriptions, or other scalar text, so a preset can still lack the real binaries section and skip the warning. Please parse the preset or at least anchor the check to a YAML key.

Suggested fix
 function validatePreset(presetContent, presetName) {
-  if (!presetContent.includes("binaries:")) {
+  let parsed = null;
+  try {
+    parsed = YAML.parse(presetContent);
+  } catch {
+    parsed = null;
+  }
+  if (
+    !parsed ||
+    typeof parsed !== "object" ||
+    !Object.prototype.hasOwnProperty.call(parsed, "binaries")
+  ) {
     console.warn(
       `  Warning: preset '${presetName}' has no binaries section — ` +
         `this will cause 403 errors in the sandbox (ref: `#676`)`,
     );
     return false;
   }
   return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/policies.js` around lines 65 - 73, The current check in
validatePreset using presetContent.includes("binaries:") can be fooled by
comments or scalars; instead parse the YAML and detect a real top-level key.
Replace the substring check in validatePreset with a YAML parse (e.g.,
yaml.parse / js-yaml.safeLoad) of presetContent and then check for
Object.prototype.hasOwnProperty.call(parsed, "binaries") (and that
parsed.binaries is not undefined/null); if parsing fails, log a warning and
treat the preset as missing the binaries section so the original warning is
emitted for presetName.

Comment on lines +8 to 9
const { readConfigFile, writeConfigFile } = require("./config-io");

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

Lock acquisition still bypasses the new config permission handling.

save() now wraps the final file write, but every mutating path reaches acquireLock() first. Permission failures creating ~/.nemoclaw or ${REGISTRY_FILE}.lock still escape as plain fs errors, so the new remediation never reaches the main registerSandbox / updateSandbox / removeSandbox / setDefault flows that issue #692 targets.

Also applies to: 124-129

Comment on lines +29 to +41
function buildRemediation(): string {
const home = process.env.HOME || os.homedir();
const nemoclawDir = path.join(home, ".nemoclaw");
return [
" To fix, run one of:",
"",
` sudo chown -R $(whoami) ${nemoclawDir}`,
` # or, if the directory was created by another user:`,
` sudo rm -rf ${nemoclawDir} && nemoclaw onboard`,
"",
" This usually happens when NemoClaw was first run with sudo",
" or the config directory was created by a different user.",
].join("\n");
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

Remediation still assumes sudo exists.

Issue #692 explicitly includes environments where sudo is unavailable, but both suggested fixes here still require it. In those installs the new ConfigPermissionError is still not actionable. Please add a non-sudo fallback, e.g. recreating config under a user-writable HOME or config directory.

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

In `@src/lib/config-io.ts` around lines 29 - 41, The current buildRemediation()
message assumes sudo is available; update it to include non-sudo fallback
actions so environments without sudo get actionable guidance. In the
buildRemediation() function (reference: nemoclawDir and HOME usage) add
alternative suggestions such as recreating the config under the current user's
home (e.g., remove or move the directory if writable), instructing to remove the
directory without sudo when the user owns it (e.g., rm -rf $HOME/.nemoclaw), and
advising to relocate or initialize config in a user-writable path (for example
creating a new config under $HOME or specifying an alternative CONFIG_HOME), so
the error message covers both sudo and non-sudo environments. Ensure the text
clearly distinguishes when sudo is required vs when the non-sudo command
applies.

cv added a commit that referenced this pull request Apr 2, 2026
Convert the last 3 blocked-by-#782 CJS modules to TypeScript:

- credentials.js → src/lib/credentials.ts
- registry.js → src/lib/registry.ts
- policies.js → src/lib/policies.ts

716 CLI tests pass. Coverage ratchet passes.
Depends on #1370 (config-io module). Relates to #924.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@ericksoa ericksoa left a comment

Choose a reason for hiding this comment

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

Nice consolidation — the deduplication of atomic-write logic from credentials.js and registry.js into a shared module is clean, and the ConfigPermissionError with remediation hints is a real UX win for #692.

A few items worth considering before merge (inline comments below).

@wscurran wscurran added NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). enhancement: refactoring labels Apr 3, 2026
cv and others added 2 commits April 3, 2026 01:35
- ensureConfigDir: chmod 0o700 on pre-existing dirs with weaker modes
  (preserves old credentials.js hardening behavior)
- readConfigFile: remove TOCTOU existsSync, catch ENOENT directly
- acquireLock: use ensureConfigDir for consistent permission errors
- applyPreset: bail early when validatePreset returns false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cv
Copy link
Copy Markdown
Contributor Author

cv commented Apr 3, 2026

All four items addressed in a63e957:

  1. chmod on pre-existing dirsensureConfigDir now stat-checks and tightens permissions when the existing mode is weaker than 0o700 (preserves the old chmodSync hardening)
  2. TOCTOU removedreadConfigFile now tries readFileSync directly and catches ENOENT alongside corrupt JSON
  3. acquireLock uses ensureConfigDir — permission failures in lock acquisition now surface ConfigPermissionError with remediation
  4. applyPreset bails early — returns false when validatePreset fails instead of continuing to apply a preset missing binaries

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@bin/lib/policies.js`:
- Around line 253-256: The calls to applyPreset (used in the resume flow,
interactive flow, and single selection flow) are ignoring its boolean return and
thus silently continuing on failure; update each call site that currently just
calls applyPreset (notably the resume, interactive and single-selection code
paths) to check the return value and handle failure the same way as the other
sites: either throw an error when applyPreset returns false or wrap the call in
the same try/catch + retry logic used at the other locations (the handlers
around applyPreset that perform retries at lines noted in the review), ensuring
failures are logged and cause the flow to abort or retry rather than silently
proceeding.
🪄 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

Run ID: c59564c8-12c4-4072-badd-41205052bfa1

📥 Commits

Reviewing files that changed from the base of the PR and between 45f3d93 and a63e957.

📒 Files selected for processing (3)
  • bin/lib/policies.js
  • bin/lib/registry.js
  • src/lib/config-io.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/config-io.ts

Comment on lines +253 to +256
if (!validatePreset(presetContent, presetName)) {
return 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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all call sites of applyPreset and check if return value is used

echo "=== Call sites of applyPreset ==="
rg -n --type js '\bapplyPreset\s*\(' -A2 -B2

echo ""
echo "=== Specifically checking if return value is captured ==="
# Look for patterns where applyPreset is called without assignment or conditional
rg -n --type js '^\s*(policies\.)?applyPreset\s*\('

Repository: NVIDIA/NemoClaw

Length of output: 3708


🏁 Script executed:

#!/bin/bash
# Check the full applyPreset function implementation
echo "=== Full applyPreset function in policies.js ==="
sed -n '236,290p' bin/lib/policies.js

echo ""
echo "=== Context around line 3193 in onboard.js ==="
sed -n '3185,3200p' bin/lib/onboard.js

Repository: NVIDIA/NemoClaw

Length of output: 2452


🏁 Script executed:

#!/bin/bash
# Check the broader context of the resume workflow to understand error handling intent
echo "=== Function context for line 3193 ==="
sed -n '3170,3210p' bin/lib/onboard.js

echo ""
echo "=== Check other unchecked call sites ==="
echo "Line 3307 context:"
sed -n '3300,3315p' bin/lib/onboard.js

echo ""
echo "Line 1119 context:"
sed -n '1110,1125p' bin/nemoclaw.js

Repository: NVIDIA/NemoClaw

Length of output: 2796


Unchecked return values at multiple call sites may leave presets silently unapplied.

The resume workflow at line 3193 calls applyPreset without checking its return value. When preset loading or validation fails, applyPreset returns false with a logged error, but the loop continues silently and the function returns chosen[] as if all presets were applied.

This pattern exists at three unchecked call sites:

  • Line 3193 (resume flow)
  • Line 3307 (interactive flow)
  • Line 1119 (single selection flow)

In contrast, other call sites (lines 3082, 3131, 3145, 3240) wrap applyPreset in try-catch blocks with retry logic. The inconsistency means some failures trigger retries while others are silently ignored, potentially leaving sandboxes in misconfigured states.

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

In `@bin/lib/policies.js` around lines 253 - 256, The calls to applyPreset (used
in the resume flow, interactive flow, and single selection flow) are ignoring
its boolean return and thus silently continuing on failure; update each call
site that currently just calls applyPreset (notably the resume, interactive and
single-selection code paths) to check the return value and handle failure the
same way as the other sites: either throw an error when applyPreset returns
false or wrap the call in the same try/catch + retry logic used at the other
locations (the handlers around applyPreset that perform retries at lines noted
in the review), ensuring failures are logged and cause the flow to abort or
retry rather than silently proceeding.

@cv cv enabled auto-merge (squash) April 3, 2026 09:00
cv added a commit that referenced this pull request Apr 3, 2026
The selectFromList function was added to policies.js on main (via #1370)
after our TS migration branched. Add the typed implementation to keep
the TS module in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement: refactoring NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EACCES: permission denied on /sandbox/.openclaw/identity/device-auth.json — curl install on Linux (no sudo in sandbox)

3 participants