Skip to content

fix(config): validate dotpath key against known top-level sections#2071

Closed
Sanjays2402 wants to merge 2 commits intoNVIDIA:mainfrom
Sanjays2402:fix/config-set-key-validation
Closed

fix(config): validate dotpath key against known top-level sections#2071
Sanjays2402 wants to merge 2 commits intoNVIDIA:mainfrom
Sanjays2402:fix/config-set-key-validation

Conversation

@Sanjays2402
Copy link
Copy Markdown

@Sanjays2402 Sanjays2402 commented Apr 19, 2026

Summary

nemoclaw config set accepted any dotpath, including invalid top-level keys that OpenClaw doesn't recognize. The config was silently written with no validation. This PR adds an allowlist that validates the top-level key from the dotpath against known sections and surfaces a clear error listing the valid options when the key is unknown.

Related Issue

Fixes #2019

Changes

  • Added a top-level section allowlist in the config-set path (after the existing gateway-section blocking).
  • Invalid keys now produce a clear error listing valid top-level sections instead of silently succeeding.

Type of Change

  • Code change (feature, bug fix, or refactor)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Manual repro: nemoclaw config set bogus.key value — before: silently written. After: rejected with a listing of valid sections.

AI Disclosure

  • AI-assisted — tool: OpenClaw (Claude Opus 4.7 via GitHub Copilot)

Summary by CodeRabbit

  • New Features
    • Configuration validation: agents can now declare supported top-level configuration sections. When declared, invalid configuration keys are rejected with helpful error messages listing valid options, reducing configuration mistakes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

Top-level config key validation for config set is now driven by an agent-declared allowlist: agent manifests can publish config.known_sections, AgentDefinition exposes it as knownConfigSections, resolveAgentConfig forwards it into AgentConfigTarget, and configSet rejects writes whose top-level key is not in that list (exiting with code 1). Defaults remain permissive when no list is provided.

Changes

Cohort / File(s) Summary
Sandbox config logic
src/lib/sandbox-config.ts
Added knownSections to AgentConfigTarget and DEFAULT_AGENT_CONFIG; resolveAgentConfig forwards the agent's known sections; configSet computes the top-level segment of --key and, when target.knownSections is non-null/non-empty, rejects writes whose top-level key is not in the allowlist (logs unknown key + sorted valid keys and exits with code 1). Step-comment numbering updated.
Agent definition parser
src/lib/agent-defs.ts
Added `AgentDefinition.knownConfigSections: string[]
Agent manifest
agents/openclaw/manifest.yaml
Introduced config.known_sections listing OpenClaw top-level config sections so the CLI can validate keys against the agent-declared allowlist.
Tests
test/config-set.test.ts
Added tests asserting manifest-driven validation: an agent without known_sections yields knownSections === null, no hardcoded allowlist exists, and validation is consulted only when target.knownSections is present and non-empty; added YAML assertions for OpenClaw (must declare) and Hermes (must omit).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI as "nemoclaw CLI"
  participant Resolver as "resolveAgentConfig"
  participant AgentDef as "AgentDefinition (manifest)"
  participant Sandbox as "configSet / sandbox-config"
  participant FS as "Sandbox FS (openclaw.json)"

  CLI->>Resolver: request agent config target (agent id)
  Resolver->>AgentDef: read manifest.raw.config
  AgentDef-->>Resolver: knownConfigSections (list or null)
  Resolver->>Sandbox: pass AgentConfigTarget{..., knownSections}
  CLI->>Sandbox: config set --key a.b.c --value v
  Sandbox->>Sandbox: topLevel = split(key)[0]
  alt knownSections is non-empty
    Sandbox->>Sandbox: if topLevel ∉ knownSections -> log error + exit(1)
  else knownSections null/empty
    Sandbox->>FS: write key/value into openclaw.json
    FS-->>CLI: success
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit read the manifest with care,
Listed sections hopped into the air.
"Only these!" it gently said,
Now bad keys stay safely in bed.
Hooray for tidy config lairs! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 reflects the primary change: adding validation of the dotpath key against known top-level sections in the config-set command.
Linked Issues check ✅ Passed The PR implements the core requirement from #2019 by validating top-level keys against a known sections allowlist and rejecting unrecognized keys with clear error messages.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the validation mechanism: adding knownSections field to agent config, parsing it from manifest.yaml, and enforcing validation in config-set.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/config-set-key-validation

Review rate limit: 8/10 reviews remaining, refill in 7 minutes and 58 seconds.

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: 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 `@src/lib/sandbox-config.ts`:
- Line 339: The step comment "// 8. Write to temp file in the agent's native
format" was inserted but the following step comment(s) still use "// 8.",
causing duplicated step numbers; update the subsequent step comment(s) in
src/lib/sandbox-config.ts (the later "// 8." occurrences) to the correct
incremented numbers (e.g., change the next "// 8." to "// 9." and continue
renumbering any further step comments) so the sequence is consistent; search for
the nearby comment text and adjust the comment tokens accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f1bba34c-4ebe-4ef0-b301-1fb462860a32

📥 Commits

Reviewing files that changed from the base of the PR and between 4e6508d and d15f1a9.

📒 Files selected for processing (1)
  • src/lib/sandbox-config.ts

Comment thread src/lib/sandbox-config.ts
@wscurran wscurran added bug Something isn't working configuration labels Apr 20, 2026
@wscurran
Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR that proposes a fix for the config validation to prevent invalid dotpaths, which could help improve the reliability of the configuration.


Possibly related open issues:

@brandonpelfrey
Copy link
Copy Markdown
Collaborator

@Sanjays2402

Thanks for tackling this — the underlying problem from #2019 is real and the error message UX is good. A couple of concerns with the approach though:

The allowlist is OpenClaw-specific, but the code path is agent-generic. configSet uses resolveAgentConfig() to dynamically resolve the agent — NemoClaw already ships a Hermes agent (agents/hermes/manifest.yaml) whose config uses completely different top-level keys (model, provider, etc.). This allowlist would reject every valid Hermes config write.

This conflicts with an existing design decision. There's already a "new key gate" at line ~481 (the classifyNewKeyGate call) that was specifically introduced to handle this problem without coupling to a hardcoded schema — see issue #2400 and the comment at line 481: "without coupling the validator to OpenClaw's evolving config schema." That issue documents a previous attempt at schema-coupled validation that was reverted because it blocked valid-but-not-yet-written keys. This PR reintroduces the same coupling.

The allowlist may already be incomplete. The original issue's repro uses the key inference.endpoint, but inference isn't in the allowlist. If OpenClaw adds new top-level sections in future releases, they'd be silently blocked here until someone updates this list.

A more robust approach might be:

  • Make the allowlist agent-aware (e.g., defined in each agent's manifest.yaml under a config.known_sections field), or
  • Lean on the existing new-key-gate mechanism and improve its error message to be more explicit about why the key looks suspicious

Would you be open to reworking the validation to be agent-aware rather than hardcoded? Happy to discuss the design.

Reject unknown top-level config keys in 'config set' to prevent
writing unrecognized paths that OpenClaw silently ignores.

Closes NVIDIA#2019

Signed-off-by: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com>
Per @brandonpelfrey's review on NVIDIA#2071: the original PR hardcoded an
OpenClaw-specific allowlist directly in src/lib/sandbox-config.ts, which
would have rejected every valid Hermes-agent config write (model, provider,
etc.) and re-introduced the schema coupling that NVIDIA#2400 explicitly avoided.

This commit pivots the validator to be agent-aware:

- Each agent's manifest.yaml may declare 'config.known_sections' under the
  existing config block. agent-defs.ts now exposes that as
  AgentDefinition.knownConfigSections (string[] | null).
- AgentConfigTarget gains a 'knownSections' field, populated from the
  resolved agent's manifest.
- configSet() validates the top-level dotpath ONLY when the resolved agent
  declares known_sections. Agents that omit the field skip validation
  entirely, so NemoClaw stays decoupled from any one agent's evolving
  config schema.
- agents/openclaw/manifest.yaml declares its known sections (llm, tts,
  stt, mcp, sandbox, extensions, personas, customization, inference,
  plugins, skills, ui, logging) so the original NVIDIA#2019 typo
  ('inferenc.endpoint') is still caught for OpenClaw users.
- agents/hermes/manifest.yaml intentionally does NOT declare a list,
  so Hermes config writes (model, provider, ...) are unaffected.
- New tests in test/config-set.test.ts verify the source no longer
  contains a hardcoded KNOWN_TOP_LEVEL_KEYS allowlist, that the validator
  reads from target.knownSections, and that the openclaw manifest declares
  the allowlist while the hermes manifest does not.

Step comments inside configSet() were also renumbered (the original PR
left a duplicate '// 8.', flagged by CodeRabbit).

Signed-off-by: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com>
@Sanjays2402 Sanjays2402 force-pushed the fix/config-set-key-validation branch from d15f1a9 to 9077e7c Compare April 29, 2026 06:37
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented Apr 29, 2026

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

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Sanjays2402
Copy link
Copy Markdown
Author

Thanks @brandonpelfrey — you're absolutely right, the original allowlist was wrong on every count you raised. Reworked the design to be agent-aware rather than hardcoded:

What changed

  1. Validator no longer hardcodes any allowlist. The KNOWN_TOP_LEVEL_KEYS set is gone from src/lib/sandbox-config.ts.
  2. Source of truth moved into each agent's manifest.yaml under a new optional config.known_sections field. src/lib/agent-defs.ts exposes it as AgentDefinition.knownConfigSections: string[] | null, threaded through AgentConfigTarget.knownSections.
  3. Validation is opt-in per agent. configSet() only validates the top-level dotpath when the resolved agent declares known_sections. If the field is absent, validation is skipped entirely — no coupling to any one agent's evolving schema (per openclaw config set isRecognizedConfigPath rejects unset keys, blocking the documented per-agent override path (and the workaround for openclaw/openclaw#64432) #2400).
  4. agents/openclaw/manifest.yaml declares its known sections (llm, tts, stt, mcp, sandbox, extensions, personas, customization, inference, plugins, skills, ui, logging), which still catches the original [All Platform] nemoclaw config set missing key validation allows writing invalid config path #2019 repro (inferenc.endpoint). Adding new sections is now a one-line manifest edit, not a code change.
  5. agents/hermes/manifest.yaml intentionally does not declare a list — Hermes config writes (model, provider, ...) are completely unaffected.
  6. CodeRabbit's // 8. duplicate step comment is fixed; subsequent steps renumbered through // 13..

Tests

New tests in test/config-set.test.ts:

  • Source no longer contains KNOWN_TOP_LEVEL_KEYS = new Set (regression guard).
  • Validator reads from target.knownSections with the documented length-guarded form.
  • agents/openclaw/manifest.yaml declares known_sections and contains essential keys (llm, sandbox, extensions).
  • agents/hermes/manifest.yaml does not declare known_sections (intentionally permissive).
  • resolveAgentConfig for an unknown sandbox falls back with knownSections: null.

npm run typecheck:cli clean. vitest run test/config-set.test.ts: 29/29 pass.

All commits DCO-signed. Force-pushed to fix/config-set-key-validation.

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: 2

🧹 Nitpick comments (1)
test/config-set.test.ts (1)

38-85: Prefer a behavioral test over source-string assertions.

These checks only grep the implementation and manifests, so they can pass even if the runtime validation regresses. A small end-to-end test that calls configSet with an invalid top-level key and asserts the exit/message would be much more durable.

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

In `@test/config-set.test.ts` around lines 38 - 85, Replace the brittle
source-string grep tests with a behavioral end-to-end test that invokes the
CLI/configSet flow: call the command (or exported function) that implements
configSet with a manifest-aware target (one that has config.known_sections set,
e.g. the OpenClaw manifest) and pass a config containing an invalid top-level
key; assert the process exits non-zero (or the function throws) and that the
error message mentions unknown/invalid top-level section(s). Keep one
lightweight assertion that sandbox-config.ts reads target.knownSections (or
remove it if you fully replace with the behavioral test), but ensure the new
test exercises the runtime path that checks target.knownSections in
sandbox-config.ts so regressions are caught at runtime instead of by string
matching.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/agent-defs.ts`:
- Around line 161-166: The getter knownConfigSections currently returns an empty
array when cfg.known_sections contains no string entries, violating the
documented contract and causing configSet to skip validation; update the
knownConfigSections getter (in src/lib/agent-defs.ts) to normalize the filtered
result so that if the filtered array of strings is empty it returns null instead
of [], i.e. after filtering (s): s is string ensure you check filtered.length
and return null when zero, otherwise return the filtered string array.

In `@test/config-set.test.ts`:
- Around line 5-9: Remove the stale eslint suppression comment before the
require call in test/config-set.test.ts: delete the line "//
eslint-disable-next-line `@typescript-eslint/no-require-imports`" so the file uses
the intentional CommonJS require for the yaml constant (const yaml =
require("js-yaml") as { load: (s: string) => unknown };) without dead
suppression comments.

---

Nitpick comments:
In `@test/config-set.test.ts`:
- Around line 38-85: Replace the brittle source-string grep tests with a
behavioral end-to-end test that invokes the CLI/configSet flow: call the command
(or exported function) that implements configSet with a manifest-aware target
(one that has config.known_sections set, e.g. the OpenClaw manifest) and pass a
config containing an invalid top-level key; assert the process exits non-zero
(or the function throws) and that the error message mentions unknown/invalid
top-level section(s). Keep one lightweight assertion that sandbox-config.ts
reads target.knownSections (or remove it if you fully replace with the
behavioral test), but ensure the new test exercises the runtime path that checks
target.knownSections in sandbox-config.ts so regressions are caught at runtime
instead of by string matching.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e7f2040f-363e-452a-a16c-135c272dbb3d

📥 Commits

Reviewing files that changed from the base of the PR and between d15f1a9 and 9077e7c.

📒 Files selected for processing (4)
  • agents/openclaw/manifest.yaml
  • src/lib/agent-defs.ts
  • src/lib/sandbox-config.ts
  • test/config-set.test.ts

Comment thread src/lib/agent-defs.ts
Comment on lines +161 to +166
get knownConfigSections(): string[] | null {
const cfg = (raw.config as Record<string, unknown>) || {};
const sections = cfg.known_sections;
if (!Array.isArray(sections) || sections.length === 0) return null;
return sections.filter((s): s is string => typeof s === "string");
},
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

Return null when the filtered allowlist is empty.

If config.known_sections contains only non-string entries, this getter currently returns [], which makes configSet silently skip validation for a malformed manifest. Normalizing an empty filtered result back to null keeps the documented contract intact.

🛠️ Suggested fix
     get knownConfigSections(): string[] | null {
       const cfg = (raw.config as Record<string, unknown>) || {};
       const sections = cfg.known_sections;
       if (!Array.isArray(sections) || sections.length === 0) return null;
-      return sections.filter((s): s is string => typeof s === "string");
+      const knownSections = sections.filter((s): s is string => typeof s === "string");
+      return knownSections.length > 0 ? knownSections : null;
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/agent-defs.ts` around lines 161 - 166, The getter knownConfigSections
currently returns an empty array when cfg.known_sections contains no string
entries, violating the documented contract and causing configSet to skip
validation; update the knownConfigSections getter (in src/lib/agent-defs.ts) to
normalize the filtered result so that if the filtered array of strings is empty
it returns null instead of [], i.e. after filtering (s): s is string ensure you
check filtered.length and return null when zero, otherwise return the filtered
string array.

Comment thread test/config-set.test.ts
Comment on lines +5 to +9
import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const yaml = require("js-yaml") as { load: (s: string) => unknown };
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:

# First, check if the test file exists and read the relevant lines
cat -n test/config-set.test.ts | head -20

Repository: NVIDIA/NemoClaw

Length of output: 1115


🏁 Script executed:

# Check the entire js-yaml import section and surrounding context
cat -n test/config-set.test.ts

Repository: NVIDIA/NemoClaw

Length of output: 8313


🏁 Script executed:

# Look for eslint configuration files
find . -maxdepth 2 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) | head -20

Repository: NVIDIA/NemoClaw

Length of output: 108


🏁 Script executed:

# Check if js-yaml is in package.json and what version
cat package.json | grep -A 5 -B 5 "js-yaml" || echo "Not found in grep, checking full file"

Repository: NVIDIA/NemoClaw

Length of output: 814


🏁 Script executed:

# Check js-yaml module to understand if it supports ESM
ls -la node_modules/js-yaml 2>/dev/null | head -20 || echo "node_modules not available, checking package.json exports"

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Look for how js-yaml is used elsewhere in the codebase
rg "js-yaml" --type ts --type js -A 2 -B 2

Repository: NVIDIA/NemoClaw

Length of output: 674


🏁 Script executed:

# Check the main eslint configuration
cat eslint.config.mjs | head -100

Repository: NVIDIA/NemoClaw

Length of output: 2966


🏁 Script executed:

# Check if `@typescript-eslint/no-require-imports` rule is mentioned
grep -r "no-require-imports" eslint.config.mjs || echo "Rule not found in eslint.config.mjs"

Repository: NVIDIA/NemoClaw

Length of output: 95


🏁 Script executed:

# Look up js-yaml package to see if it has ESM exports
cat package.json | jq '.dependencies' || cat package.json | grep -A 10 '"dependencies"'

Repository: NVIDIA/NemoClaw

Length of output: 128


🏁 Script executed:

# Check if js-yaml has ESM support by looking at common patterns
web_search
js-yaml npm package ESM import support version 4.1

Repository: NVIDIA/NemoClaw

Length of output: 156


🌐 Web query:

js-yaml npm version 4.1.1 ESM import support

💡 Result:

Answer: js-yaml@4.1.1 does not provide native ESM import support for Node.js as a package-level feature; it’s primarily CommonJS, and the project historically declined to rewrite for ESM/ES6 import syntax. In practice, you can still use it from ESM via default/CommonJS interop, or by importing a dedicated ESM build from a CDN path (not the npm package entrypoint).

Citations:


🏁 Script executed:

# Verify: check if there are any other uses of this suppression in the codebase
rg "@typescript-eslint/no-require-imports" --type ts --type js

Repository: NVIDIA/NemoClaw

Length of output: 1033


🏁 Script executed:

# Double-check the test section of eslint.config.mjs for any missed rules
grep -A 30 'files:.*test' eslint.config.mjs

Repository: NVIDIA/NemoClaw

Length of output: 1640


Remove the stale eslint suppression comment.

The @typescript-eslint/no-require-imports rule is not configured in eslint.config.mjs, so the suppression has no effect and should be removed as dead code. The require() here is intentional; eslint's test configuration explicitly allows require in test files to support CommonJS interop (e.g., js-yaml v4.1.1, which lacks native ESM support). No import pattern change is needed.

🧰 Tools
🪛 ESLint

[error] 8-8: Definition for rule '@typescript-eslint/no-require-imports' was not found.

(@typescript-eslint/no-require-imports)

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

In `@test/config-set.test.ts` around lines 5 - 9, Remove the stale eslint
suppression comment before the require call in test/config-set.test.ts: delete
the line "// eslint-disable-next-line `@typescript-eslint/no-require-imports`" so
the file uses the intentional CommonJS require for the yaml constant (const yaml
= require("js-yaml") as { load: (s: string) => unknown };) without dead
suppression comments.

@ericksoa ericksoa added the status: superseded Another PR has resolved the intent of this PR. label May 5, 2026
@ericksoa
Copy link
Copy Markdown
Contributor

ericksoa commented May 5, 2026

Thanks @Sanjays2402 for the contribution here. This PR correctly targeted the #2019 config-set validation gap and helped move the design discussion toward avoiding silent writes for typoed config paths.

We have since landed the accepted fix in #2036, which resolved #2019 using the current new-key gate design: unknown paths are refused by default in non-interactive mode unless the user explicitly opts in to creating a new path. That keeps the validator schema-free per the #2400 design constraints while still protecting users from accidental typo writes. Since the issue is now resolved on main by that approach, I am closing this PR as superseded rather than rejected on merit.

@ericksoa ericksoa closed this May 5, 2026
@Sanjays2402 Sanjays2402 deleted the fix/config-set-key-validation branch May 5, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working status: superseded Another PR has resolved the intent of this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[All Platform] nemoclaw config set missing key validation allows writing invalid config path

4 participants