Skip to content

fix(security): validate Hermes endpoint URL and reject allowed_ips in user presets - #6085

Closed
prekshivyas wants to merge 5 commits into
mainfrom
fix/ssrf-validation-gaps-6072-6073
Closed

fix(security): validate Hermes endpoint URL and reject allowed_ips in user presets#6085
prekshivyas wants to merge 5 commits into
mainfrom
fix/ssrf-validation-gaps-6072-6073

Conversation

@prekshivyas

@prekshivyas prekshivyas commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes two SSRF gaps found during a security review of NemoClaw's policy and onboarding flows. Neither gap was exploitable through the sandboxed agent — both required user-level access to the CLI.

Related Issue

Fixes #6072
Fixes #6073

Changes

  • src/lib/onboard/inference-providers/hermes.ts: call isPrivateHostname() on the user-supplied endpointUrl before it is persisted and passed to OpenShell as OPENAI_BASE_URL. Previously the URL bypassed all SSRF checks until plugin-side validateEndpointUrl() fired at inference time — after credentials could already have been dispatched to an internal host.
  • src/lib/policy/index.ts: reject user-supplied preset files (--from-file / --from-dir) that declare allowed_ips in any network_policies endpoint. The merge was previously a blind structural pass-through that let callers expand the private-IP allowlist that OpenShell enforces.
  • src/lib/onboard/inference-providers/hermes.test.ts: 8 new unit tests covering loopback, link-local (169.254.x.x), RFC-1918, .internal TLD, malformed URLs, public endpoint acceptance, and null pass-through.
  • src/lib/policy/preset-allowed-ips.test.ts: 4 new unit tests covering single-policy rejection, multi-policy rejection, clean preset acceptance, and endpoint-without-allowed_ips acceptance.

Type of Change

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

Quality Gates

Verification

  • PR description includes the DCO sign-off declaration and every commit appears as Verified in GitHub
  • Git hooks passed during commit and push, or npx prek run --from-ref main --to-ref HEAD passes
  • Targeted tests pass for changed behavior
  • No secrets, API keys, or credentials committed

Signed-off-by: Prekshi Vyas prekshiv@nvidia.com

Summary by CodeRabbit

  • Bug Fixes
    • Improved startup handling when a custom command is provided, ensuring permission normalization still runs afterward and the original exit code is preserved.
    • Added an SSRF safeguard for inference endpoints by rejecting private/internal hostnames and invalid URLs.
    • Tightened preset YAML validation to reject allowed_ips inside network endpoint entries.
  • Tests
    • Added regression tests covering the startup command/normalization order and exit-code behavior.
    • Added tests for inference endpoint validation and for rejecting allowed_ips in presets.

prekshivyas and others added 4 commits June 30, 2026 11:50
openclaw doctor --fix collapses /sandbox/.openclaw from 2770 to 700
and openclaw.json from 660 to 600 when run via `nemoclaw exec`. The
NEMOCLAW_CMD exec paths used bare `exec` to replace the shell process,
making any post-command cleanup impossible.

Replace `exec` with a regular call, capture the exit code, call
normalize_mutable_config_perms to restore 2770/660, then exit with
the original code. Covers both the non-root path and the root path
(via STEP_DOWN_PREFIX_SANDBOX).

Fixes #6047

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
openclaw doctor --fix collapses /sandbox/.openclaw from 2770 to 700
and openclaw.json from 660 to 600 when run via `nemoclaw exec`. The
NEMOCLAW_CMD exec paths used bare `exec` to replace the shell process,
making any post-command cleanup impossible.

Replace `exec` with a regular call in both NEMOCLAW_CMD paths. Use
`_nemoclaw_cmd_rc=0; cmd || _nemoclaw_cmd_rc=$?` to capture the exit
code safely under `set -e`, call normalize_mutable_config_perms to
restore 2770/660, then exit with the original code. Covers both the
non-root path and the root path (via STEP_DOWN_PREFIX_SANDBOX).

Add ORDER-marker tests in test/nemoclaw-start-perms.test.ts to verify
the call sequence (command → normalize → exit with captured code) in
both paths, including that a non-zero exit from NEMOCLAW_CMD is
preserved through the normalize call.

Fixes #6047

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-shape asserts

The codebase-growth-guardrails check bans `if (` in changed test files and
caps source-shape assertion cases at 0. Move the script-block extraction into
module-scope helpers (assertions there are not counted as source-shape cases)
and drop the inline if/throw guards, keeping the test bodies linear.

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… user presets

Fixes two SSRF gaps identified in issues #6072 and #6073.

1. src/lib/onboard/inference-providers/hermes.ts: call isPrivateHostname()
   on the user-supplied endpointUrl before passing it downstream as
   OPENAI_BASE_URL. Previously the URL bypassed all SSRF checks until
   plugin-side validateEndpointUrl() fired at inference time — too late
   to prevent credential dispatch to an internal host.

2. src/lib/policy/index.ts: reject user-supplied preset files that
   declare allowed_ips in any network_policies endpoint. The merge was
   previously a blind structural pass-through that let callers expand
   the private-IP allowlist OpenShell enforces.

Fixes #6072
Fixes #6073

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1e5150da-4f7a-4279-a0d2-646a446cd1b5

📥 Commits

Reviewing files that changed from the base of the PR and between 2698ec2 and 190950e.

📒 Files selected for processing (1)
  • src/lib/onboard/inference-providers/hermes.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/onboard/inference-providers/hermes.test.ts

📝 Walkthrough

Walkthrough

This PR adds SSRF validation for Hermes inference endpoint URLs, rejects allowed_ips in user-supplied policy presets, and changes nemoclaw-start.sh to preserve command exit codes after re-normalizing config permissions. Each change includes regression tests.

Changes

Hermes SSRF Validation

Layer / File(s) Summary
SSRF guard implementation in Hermes provider setup
src/lib/onboard/inference-providers/hermes.ts
Imports isPrivateHostname and validates endpointUrl, rejecting invalid URLs and private/internal hostnames.
SSRF guard test suite
src/lib/onboard/inference-providers/hermes.test.ts
Tests reject loopback, metadata, RFC1918, localhost, and .internal endpoints; verify invalid URL handling; confirm public and null endpoints are accepted.

Policy allowed_ips Guard

Layer / File(s) Summary
allowed_ips rejection in loadPresetFromFile
src/lib/policy/index.ts
loadPresetFromFile now scans network_policies[*].endpoints[*] and returns null when any endpoint contains allowed_ips.
allowed_ips guard test suite
src/lib/policy/preset-allowed-ips.test.ts
Tests cover rejection when allowed_ips appears in any policy entry and acceptance when it is absent.

Entrypoint Exit Code Preservation

Layer / File(s) Summary
Capture exit code and normalize perms after command run
scripts/nemoclaw-start.sh
Non-root and root/step-down NEMOCLAW_CMD branches now run the command, capture its exit code, re-run normalize_mutable_config_perms, then exit with the captured code.
Regression tests for exit code ordering
test/nemoclaw-start-perms.test.ts
Tests extract the relevant script blocks and assert command output precedes normalization output, the exit status matches the command code, and unreachable code is not reached.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant OnboardFlow
  participant setupHermesProviderInference
  participant isPrivateHostname
  participant runOpenshell

  User->>OnboardFlow: supply endpointUrl
  OnboardFlow->>setupHermesProviderInference: setupHermesProviderInference(endpointUrl)
  setupHermesProviderInference->>setupHermesProviderInference: parse URL
  alt invalid URL
    setupHermesProviderInference-->>OnboardFlow: throw Invalid inference endpoint URL
  else valid URL
    setupHermesProviderInference->>isPrivateHostname: check hostname
    isPrivateHostname-->>setupHermesProviderInference: private/internal?
    alt private or internal
      setupHermesProviderInference-->>OnboardFlow: throw private-endpoint error
    else public
      setupHermesProviderInference->>runOpenshell: proceed
    end
  end
Loading
sequenceDiagram
  participant Shell
  participant NEMOCLAW_CMD
  participant normalize_mutable_config_perms
  participant ExitStatus

  Shell->>NEMOCLAW_CMD: run command
  NEMOCLAW_CMD-->>Shell: exit code
  Shell->>normalize_mutable_config_perms: re-normalize permissions
  normalize_mutable_config_perms-->>Shell: done
  Shell->>ExitStatus: exit with captured code
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • NVIDIA/NemoClaw#898: Updates the shared SSRF validation path and edge-case coverage related to private/internal hostname checks.
  • NVIDIA/NemoClaw#1557: Expands the private/internal SSRF denylist used by hostname validation.
  • NVIDIA/NemoClaw#6073: Related policy preset validation work around allowed_ips handling.

Suggested labels

security, bug-fix, area: inference, area: policy, area: onboarding

Suggested reviewers

  • ericksoa
  • cv
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The nemoclaw-start shell change and its regression test are unrelated to the linked security issues. Move the nemoclaw-start permission/exit-code changes to a separate PR unless they are required for #6072 or #6073.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 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 two security fixes in Hermes endpoint validation and preset allowed_ips rejection.
Linked Issues check ✅ Passed The Hermes SSRF validation and preset allowed_ips rejection match the requirements of issues #6072 and #6073.
✨ 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 fix/ssrf-validation-gaps-6072-6073

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

@github-code-quality

github-code-quality Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in the fix/ssrf-validation-... branch is 96%. Coverage data for the main branch is not yet available.

Show a code coverage summary of the most covered files.
File main fix/ssrf-validation-... 190950e +/-
nemoclaw/src/se...cret-scanner.ts 100%
nemoclaw/src/commands/slash.ts 100%
nemoclaw/src/li...bprocess-env.ts 100%
nemoclaw/src/bl...eprint/state.ts 98%
nemoclaw/src/onboard/config.ts 98%
nemoclaw/src/bl...int/snapshot.ts 97%
nemoclaw/src/bl...print/runner.ts 95%
nemoclaw/src/co...ration-state.ts 94%
nemoclaw/src/bl...ate-networks.ts 94%
nemoclaw/src/index.ts 94%

TypeScript / code-coverage/cli

The overall coverage in the fix/ssrf-validation-... branch is 68%. Coverage data for the main branch is not yet available.

Show a code coverage summary of the most covered files.
File main fix/ssrf-validation-... 190950e +/-
src/lib/actions...dbox/rebuild.ts 80%
src/lib/actions...all/run-plan.ts 80%
src/lib/state/o...oard-session.ts 80%
src/lib/state/sandbox.ts 72%
src/lib/onboard/preflight.ts 69%
src/lib/shields/index.ts 67%
src/lib/onboard...er-gpu-patch.ts 59%
src/lib/actions...licy-channel.ts 58%
src/lib/policy/index.ts 57%
src/lib/onboard.ts 20%

Updated July 01, 2026 02:00 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor (Nemotron Ultra) — Changes requested

Merge posture: Do not merge yet
Primary next action: Fix PRA-2: Non-root entrypoint: permission restore after exec preserves exit code; then add or justify PRA-T1.
Open items: 5 required · 5 warnings · 3 suggestions · 8 test follow-ups
Since last review: 4 prior items resolved · 2 still apply · 4 new items found

Action checklist

  • PRA-2 Fix: Non-root entrypoint: permission restore after exec preserves exit code in scripts/nemoclaw-start.sh:3995
  • PRA-3 Fix: Root/step-down entrypoint: permission restore after exec preserves exit code in scripts/nemoclaw-start.sh:4115
  • PRA-4 Fix: Hermes onboarding SSRF guard validates endpointUrl at trust boundary in src/lib/onboard/inference-providers/hermes.ts:35
  • PRA-5 Fix: Policy preset load rejects allowed_ips in user-supplied presets in src/lib/policy/index.ts:1025
  • PRA-6 Fix: Built-in presets declare allowed_ips with broad RFC1918 ranges — guard only applies to user presets in nemoclaw-blueprint/policies/presets/*.yaml
  • PRA-1 Resolve or justify: Source-of-truth review needed: Permission restore after exec (normalize_mutable_config_perms)
  • PRA-7 Resolve or justify: Null endpointUrl skips SSRF check — verify downstream safety in src/lib/onboard/inference-providers/hermes.ts:39
  • PRA-8 Resolve or justify: Remote providers lack onboarding-time SSRF validation in src/lib/onboard/inference-providers/remote.ts:56
  • PRA-9 Resolve or justify: Test couples to script formatting via string slicing in test/nemoclaw-start-perms.test.ts:25
  • PRA-10 Resolve or justify: Document permission restore workaround rationale in scripts/nemoclaw-start.sh:3995
  • PRA-T1 Add or justify test follow-up: Runtime validation
  • PRA-T2 Add or justify test follow-up: Runtime validation
  • PRA-T3 Add or justify test follow-up: Runtime validation
  • PRA-T4 Add or justify test follow-up: Runtime validation
  • PRA-T5 Add or justify test follow-up: Runtime validation
  • PRA-T6 Add or justify test follow-up: Test couples to script formatting via string slicing
  • PRA-T7 Add or justify test follow-up: Permission restore after exec (normalize_mutable_config_perms)
  • PRA-T8 Add or justify test follow-up: Null endpointUrl skips SSRF check
  • PRA-11 In-scope improvement: Extract shared allowed_ips validation for user and built-in preset paths in src/lib/policy/index.ts:1025
  • PRA-12 In-scope improvement: Test mock for isPrivateHostname duplicates canonical logic and may drift in src/lib/onboard/inference-providers/hermes.test.ts:9
  • PRA-13 In-scope improvement: Add onboarding-time SSRF check for compatible-endpoint provider in src/lib/onboard/inference-providers/remote.ts:56

Findings index

ID Severity Category Location Required action
PRA-1 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-2 Required security scripts/nemoclaw-start.sh:3995 Fix verified in diff. Run test/nemoclaw-start-perms.test.ts to confirm. Integration test in real sandbox would strengthen confidence.
PRA-3 Required security scripts/nemoclaw-start.sh:4115 Fix verified in diff. Run test/nemoclaw-start-perms.test.ts second test case.
PRA-4 Required security src/lib/onboard/inference-providers/hermes.ts:35 Implementation correct — uses canonical block list. Ensure blueprint YAML stays in sync (parity test enforces this).
PRA-5 Required security src/lib/policy/index.ts:1025 Guard correctly placed at preset load boundary. Audit built-in presets separately to confirm none declare allowed_ips inappropriately (see PRA-7).
PRA-6 Required security nemoclaw-blueprint/policies/presets/*.yaml Audit each built-in preset: confirm allowed_ips is necessary and minimally scoped. Add explicit allowlist comment in each preset YAML explaining why RFC1918 is needed for host.openshell.internal. Consider extending validation to built-in presets with an allowlist of approved preset names.
PRA-7 Resolve/justify security src/lib/onboard/inference-providers/hermes.ts:39 Verify downstream code paths handle null endpointUrl without defaulting to private addresses. Add comment documenting null-skip rationale (e.g., 'null means use default public endpoint configured elsewhere').
PRA-8 Resolve/justify security src/lib/onboard/inference-providers/remote.ts:56 Add isPrivateHostname check in setupRemoteProviderInference before upsertProvider, matching Hermes pattern. For compatible-endpoint specifically, consider full validateEndpointUrl with DNS pinning since endpoint is user-controlled.
PRA-9 Resolve/justify tests test/nemoclaw-start-perms.test.ts:25 Acceptable for this PR. Consider extracting command-execution logic into a sourced shell function for independent unit testing in future.
PRA-10 Resolve/justify architecture scripts/nemoclaw-start.sh:3995 Add code comment near normalize_mutable_config_perms call in both non-root (line 3995) and root (line 4115) paths explaining: why permissions get mutated during exec, why restore runs after, and that this is a workaround for #6047.
PRA-11 Improvement correctness src/lib/policy/index.ts:1025 Extract validation logic to shared helper validatePresetNoAllowedIps(presetContent, presetName, isUserSupplied) for consistency. Call from both loadPresetFromFile and loadPreset (with isUserSupplied flag controlling error vs allow behavior for built-ins).
PRA-12 Improvement correctness src/lib/onboard/inference-providers/hermes.test.ts:9 Use real isPrivateHostname implementation in tests with a test-specific private-networks.yaml fixture, or add test that verifies mock classification matches canonical implementation for all entries in private-networks.yaml.
PRA-13 Improvement security src/lib/onboard/inference-providers/remote.ts:56 Add isPrivateHostname check in setupRemoteProviderInference for compatible-endpoint specifically, or for all remote providers. Consider full validateEndpointUrl with DNS pinning for compatible-endpoint since endpoint is entirely user-controlled.

🚨 Required before merge

Address these before merging unless a maintainer explicitly overrides the advisor with rationale.

PRA-2 Required — Non-root entrypoint: permission restore after exec preserves exit code

  • Location: scripts/nemoclaw-start.sh:3995
  • Category: security
  • Problem: Fixed [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047: non-root path now runs normalize_mutable_config_perms after NEMOCLAW_CMD and preserves exit code. Previously exec replaced the shell, skipping the restore and leaving .openclaw world-writable.
  • Impact: Without this fix, nemoclaw exec in non-root containers (--security-opt=no-new-privileges) collapses .openclaw permissions, enabling sandbox escape via world-writable config.
  • Required action: Fix verified in diff. Run test/nemoclaw-start-perms.test.ts to confirm. Integration test in real sandbox would strengthen confidence.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run test/nemoclaw-start-perms.test.ts — extracts non-root cmd block from real script and asserts ORDER:cmd then ORDER:normalize and exit code 42 preserved.
  • Missing regression test: Integration test in OpenShell sandbox with --security-opt=no-new-privileges running nemoclaw exec and verifying .openclaw perms post-exec.
  • Done when: The required change is committed and verification passes: Run test/nemoclaw-start-perms.test.ts — extracts non-root cmd block from real script and asserts ORDER:cmd then ORDER:normalize and exit code 42 preserved.
  • Evidence: Diff shows exec replaced with command execution + normalize_mutable_config_perms + exit $_nemoclaw_cmd_rc. Test extracts shell block and asserts order and exit code.

PRA-3 Required — Root/step-down entrypoint: permission restore after exec preserves exit code

  • Location: scripts/nemoclaw-start.sh:4115
  • Category: security
  • Problem: Fixed [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047: root path (setpriv/gosu step-down) now runs normalize_mutable_config_perms after NEMOCLAW_CMD via STEP_DOWN_PREFIX_SANDBOX and preserves exit code. Same fix as non-root path for privileged containers.
  • Impact: Without this fix, nemoclaw exec in root-mode containers leaves .openclaw permissions mutated after arbitrary user commands run as sandbox user.
  • Required action: Fix verified in diff. Run test/nemoclaw-start-perms.test.ts second test case.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run test/nemoclaw-start-perms.test.ts second test case — extracts root cmd block and asserts ORDER:cmd then ORDER:normalize with exit code 42.
  • Missing regression test: Integration test in root-mode container (no --security-opt=no-new-privileges) running nemoclaw exec and verifying .openclaw perms post-exec.
  • Done when: The required change is committed and verification passes: Run test/nemoclaw-start-perms.test.ts second test case — extracts root cmd block and asserts ORDER:cmd then ORDER:normalize with exit code 42.
  • Evidence: Diff shows same pattern: command execution via STEP_DOWN_PREFIX_SANDBOX, then normalize_mutable_config_perms, then exit with captured code.

PRA-4 Required — Hermes onboarding SSRF guard validates endpointUrl at trust boundary

  • Location: src/lib/onboard/inference-providers/hermes.ts:35
  • Category: security
  • Problem: Added SSRF validation for Hermes provider endpointUrl using shared isPrivateHostname from private-networks. Rejects loopback, RFC1918, cloud metadata (169.254.169.254), .internal TLD, localhost, and all ranges in nemoclaw-blueprint/private-networks.yaml. Closes gap where URL was validated only at inference time (after credentials dispatched).
  • Impact: Prevents user from pointing Hermes onboarding at internal addresses (cloud metadata, loopback, private ranges) which could leak credentials or enable SSRF before plugin-side validation fires.
  • Required action: Implementation correct — uses canonical block list. Ensure blueprint YAML stays in sync (parity test enforces this).
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run src/lib/onboard/inference-providers/hermes.test.ts — 8 cases cover loopback, cloud metadata, RFC1918, localhost, .internal, malformed URL, public HTTPS, and null endpointUrl skip.
  • Missing regression test: E2E test: nemoclaw onboard --agent hermes with malicious endpointUrl should fail with 'private or internal address' error.
  • Done when: The required change is committed and verification passes: Run src/lib/onboard/inference-providers/hermes.test.ts — 8 cases cover loopback, cloud metadata, RFC1918, localhost, .internal, malformed URL, public HTTPS, and null endpointUrl skip.
  • Evidence: hermes.ts:35-43 adds URL parsing + isPrivateHostname check. hermes.test.ts 8 cases cover all attack vectors.

PRA-5 Required — Policy preset load rejects allowed_ips in user-supplied presets

  • Location: src/lib/policy/index.ts:1025
  • Category: security
  • Problem: Added validation in loadPresetFromFile iterating all network_policies endpoints and returning null with descriptive error if allowed_ips field present. Prevents policy bypass where custom preset could widen egress beyond operator intent (issue security(policy): allowed_ips in user-supplied policy presets not validated before applying to OpenShell #6073).
  • Impact: Blocks user-supplied presets from expanding OpenShell's private-IP allowlist (currently only 10.200.0.2 for gateway dial-back). Without this, a preset with allowed_ips: [10.0.0.0/8] would grant access to entire private networks.
  • Required action: Guard correctly placed at preset load boundary. Audit built-in presets separately to confirm none declare allowed_ips inappropriately (see PRA-7).
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run src/lib/policy/preset-allowed-ips.test.ts — rejects preset with allowed_ips in first policy, rejects in second policy, accepts valid preset without allowed_ips, accepts endpoints omitting allowed_ips entirely.
  • Missing regression test: E2E test: nemoclaw policy add with a preset containing allowed_ips should be rejected at load time.
  • Done when: The required change is committed and verification passes: Run src/lib/policy/preset-allowed-ips.test.ts — rejects preset with allowed_ips in first policy, rejects in second policy, accepts valid preset without allowed_ips, accepts endpoints omitting allowed_ips entirely.
  • Evidence: policy/index.ts:1025-1037 iterates network_policies endpoints and rejects on allowed_ips. preset-allowed-ips.test.ts 4 cases cover rejection/acceptance.

PRA-6 Required — Built-in presets declare allowed_ips with broad RFC1918 ranges — guard only applies to user presets

  • Location: nemoclaw-blueprint/policies/presets/*.yaml
  • Category: security
  • Problem: Found 8 built-in presets with allowed_ips: local-inference, nous-web, nous-browser, nous-code, nous-image, nous-audio, openclaw-diagnostics-otel-local. All use host.openshell.internal with allowed_ips: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. This widens egress beyond default single-IP allowlist (10.200.0.2 for gateway dial-back). While legitimate for local inference/browser/diagnostics, the disparity between built-in and user preset validation creates a trust boundary inconsistency.
  • Impact: Built-in presets bypass the new validation in loadPresetFromFile which only checks user-supplied presets. An attacker who can influence built-in preset selection (unlikely) or a supply-chain compromise of a built-in preset could widen egress. More importantly, the security model is inconsistent: user presets are blocked from allowed_ips but built-in presets with broad RFC1918 ranges are allowed.
  • Required action: Audit each built-in preset: confirm allowed_ips is necessary and minimally scoped. Add explicit allowlist comment in each preset YAML explaining why RFC1918 is needed for host.openshell.internal. Consider extending validation to built-in presets with an allowlist of approved preset names.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run: grep -r allowed_ips nemoclaw-blueprint/policies/presets/ — shows 8 presets with RFC1918 ranges.
  • Missing regression test: Test that built-in preset allowed_ips entries are reviewed and documented with purpose field.
  • Done when: The required change is committed and verification passes: Run: grep -r allowed_ips nemoclaw-blueprint/policies/presets/ — shows 8 presets with RFC1918 ranges.
  • Evidence: local-inference.yaml, nous-web.yaml, nous-browser.yaml, nous-code.yaml, nous-image.yaml, nous-audio.yaml, openclaw-diagnostics-otel-local.yaml all declare allowed_ips with RFC1918 ranges for host.openshell.internal endpoints.
Review findings by urgency: 5 required fixes, 5 items to resolve/justify, 3 in-scope improvements

⚠️ Resolve or justify before merge

Investigate these in the current review; either fix them, explain why they are not applicable, or document the accepted risk.

PRA-1 Resolve/justify — Source-of-truth review needed: Permission restore after exec (normalize_mutable_config_perms)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: test/nemoclaw-start-perms.test.ts asserts ORDER:cmd → ORDER:normalize + exit code preserved
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: normalize_mutable_config_perms called at lines 3983 and 4152 in nemoclaw-start.sh after command execution

PRA-7 Resolve/justify — Null endpointUrl skips SSRF check — verify downstream safety

  • Location: src/lib/onboard/inference-providers/hermes.ts:39
  • Category: security
  • Problem: When endpointUrl is null, the guard is bypassed (if (endpointUrl) { ... }) and onboarding proceeds. Downstream consumers of Hermes provider config must handle null endpointUrl safely (not default to localhost/private). verifyOnboardInferenceSmoke receives null and falls back to INFERENCE_ROUTE_URL (https://inference.local/v1 — resolves to private via host.openshell.internal).
  • Impact: If null endpointUrl results in a private/default endpoint being configured in the gateway, the SSRF guard is effectively bypassed for the default case.
  • Recommended action: Verify downstream code paths handle null endpointUrl without defaulting to private addresses. Add comment documenting null-skip rationale (e.g., 'null means use default public endpoint configured elsewhere').
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check callers of setupHermesProviderInference and downstream inference setup — ensure null endpointUrl doesn't resolve to localhost/private IP. Trace: hermes.ts:39 skips check → deps.runOpenshell called with inference set command → verifyInferenceRoute/verifyOnboardInferenceSmoke called with null endpointUrl → falls back to INFERENCE_ROUTE_URL.
  • Missing regression test: Test that null endpointUrl flows through to a safe public default, not a private address.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check callers of setupHermesProviderInference and downstream inference setup — ensure null endpointUrl doesn't resolve to localhost/private IP. Trace: hermes.ts:39 skips check → deps.runOpenshell called with inference set command → verifyInferenceRoute/verifyOnboardInferenceSmoke called with null endpointUrl → falls back to INFERENCE_ROUTE_URL.
  • Evidence: hermes.ts:32 guards with if (endpointUrl). hermes.test.ts:177-188 explicitly tests and expects null to skip check and proceed. onboard-probes.ts:930 shows INFERENCE_ROUTE_URL = https://inference.local/v1 as fallback.

PRA-8 Resolve/justify — Remote providers lack onboarding-time SSRF validation

  • Location: src/lib/onboard/inference-providers/remote.ts:56
  • Category: security
  • Problem: Only Hermes provider has onboarding SSRF check. setupRemoteProviderInference (used by nvidia-nim, openai, anthropic, gemini, compatible-endpoint, bedrock) passes endpointUrl through unvalidated to upsertProvider. Plugin-side validateEndpointUrl catches at inference time with DNS pinning, but after URL is already persisted.
  • Impact: User could supply private endpoint during onboarding for remote providers. Credentials could be dispatched to internal host before plugin validation fires. Consistent trust boundary requires onboarding validation for all providers.
  • Recommended action: Add isPrivateHostname check in setupRemoteProviderInference before upsertProvider, matching Hermes pattern. For compatible-endpoint specifically, consider full validateEndpointUrl with DNS pinning since endpoint is user-controlled.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Trace remote.ts:56-67 — resolvedEndpointUrl = endpointUrl || config.endpointUrl passed to upsertProvider without validation.
  • Missing regression test: E2E test: nemoclaw onboard with compatible-endpoint and malicious endpointUrl should fail at onboarding, not at inference time.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Trace remote.ts:56-67 — resolvedEndpointUrl = endpointUrl || config.endpointUrl passed to upsertProvider without validation.
  • Evidence: remote.ts has no isPrivateHostname import or call. Hermes fix is in hermes.ts only.

PRA-9 Resolve/justify — Test couples to script formatting via string slicing

  • Location: test/nemoclaw-start-perms.test.ts:25
  • Category: tests
  • Problem: Module-scope helpers (nonRootCmdBlock, rootCmdBlock) slice the real script by exact string matching (markers, whitespace). Pragmatic but fragile — if nemoclaw-start.sh is reformatted or refactored, tests may break despite unchanged behavior.
  • Impact: Test maintenance burden; false negatives if script formatting changes. Acceptable for this PR but consider extraction for long-term stability.
  • Recommended action: Acceptable for this PR. Consider extracting command-execution logic into a sourced shell function for independent unit testing in future.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect test helpers at lines 14-34 — they use indexOf/slice on raw script text with exact marker strings like '# ── Non-root fallback' and ' if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then\n'.
  • Missing regression test: N/A — architectural improvement, not a bug. Future: refactor test to use behavioral contract (sourced shell library) rather than string slicing.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect test helpers at lines 14-34 — they use indexOf/slice on raw script text with exact marker strings like '# ── Non-root fallback' and ' if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then\n'.
  • Evidence: Test reads START_SCRIPT, slices by markers, injects into bash -c. Breaks if whitespace or comments change.

PRA-10 Resolve/justify — Document permission restore workaround rationale

  • Location: scripts/nemoclaw-start.sh:3995
  • Category: architecture
  • Problem: The normalize_mutable_config_perms call after exec is a workaround for exec mutating .openclaw permissions. No code comment explains the pattern or its rationale near the call site.
  • Impact: Future maintainers may not understand why normalization runs after command execution, potentially removing it and reintroducing [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047.
  • Recommended action: Add code comment near normalize_mutable_config_perms call in both non-root (line 3995) and root (line 4115) paths explaining: why permissions get mutated during exec, why restore runs after, and that this is a workaround for [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check lines around 3995 and 4115 in nemoclaw-start.sh for comments about permission restore.
  • Missing regression test: N/A — documentation improvement.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check lines around 3995 and 4115 in nemoclaw-start.sh for comments about permission restore.
  • Evidence: normalize_mutable_config_perms called at lines 3983 and 4152 with no explanatory comment nearby.

💡 In-scope improvements

These are lower-risk, not throwaway. Prefer fixing them in this PR when they are local to changed code; defer only with rationale or a linked follow-up.

PRA-11 Improvement — Extract shared allowed_ips validation for user and built-in preset paths

  • Location: src/lib/policy/index.ts:1025
  • Category: correctness
  • Problem: allowed_ips validation only in loadPresetFromFile (user presets), not in loadPreset (built-in presets). Two code paths with different validation. Intentional (trusted vs untrusted) but creates maintenance risk.
  • Impact: Inconsistent validation logic; built-in preset audit requires manual grep. Shared helper would improve auditability and prevent drift.
  • Suggested action: Extract validation logic to shared helper validatePresetNoAllowedIps(presetContent, presetName, isUserSupplied) for consistency. Call from both loadPresetFromFile and loadPreset (with isUserSupplied flag controlling error vs allow behavior for built-ins).
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Compare loadPresetFromFile (lines 1025-1037) vs loadPreset (lines 80-90) — no shared validation.
  • Missing regression test: Shared validation function tested for both user and built-in preset paths.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: loadPresetFromFile iterates np = parsed.network_policies and rejects on allowed_ips. loadPreset only reads file content, no validation.

PRA-12 Improvement — Test mock for isPrivateHostname duplicates canonical logic and may drift

  • Location: src/lib/onboard/inference-providers/hermes.test.ts:9
  • Category: correctness
  • Problem: Test mock reimplements private hostname detection (localhost, host.docker.internal, RFC1918 patterns, .internal/.local TLDs) instead of using real implementation. Parity test exists but mock could diverge from private-networks.yaml.
  • Impact: If canonical block list changes (new CIDR or name entry), test mock won't reflect it, giving false confidence.
  • Suggested action: Use real isPrivateHostname implementation in tests with a test-specific private-networks.yaml fixture, or add test that verifies mock classification matches canonical implementation for all entries in private-networks.yaml.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Compare mock at lines 9-22 against private-networks.yaml entries and private-networks.ts isPrivateHostname.
  • Missing regression test: Test that mock isPrivateHostname classification matches canonical implementation for all entries in private-networks.yaml.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: Mock at lines 9-22 defines privateHosts Set and privatePatterns regexes. Canonical implementation in private-networks.ts loads from YAML and uses BlockList.

PRA-13 Improvement — Add onboarding-time SSRF check for compatible-endpoint provider

  • Location: src/lib/onboard/inference-providers/remote.ts:56
  • Category: security
  • Problem: compatible-endpoint allows fully user-controlled endpointUrl. Currently no SSRF validation at onboarding — only plugin-side validateEndpointUrl at inference time. This is the highest-risk provider for SSRF.
  • Impact: User can point compatible-endpoint at internal hosts during onboarding. Credentials dispatched before plugin validation.
  • Suggested action: Add isPrivateHostname check in setupRemoteProviderInference for compatible-endpoint specifically, or for all remote providers. Consider full validateEndpointUrl with DNS pinning for compatible-endpoint since endpoint is entirely user-controlled.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Trace remote.ts:102 — compatible-endpoint branch uses LOCAL_INFERENCE_TIMEOUT_SECS but no SSRF check on resolvedEndpointUrl.
  • Missing regression test: E2E test: nemoclaw onboard with compatible-endpoint and http://169.254.169.254/ should fail at onboarding.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: remote.ts imports private-networks functions? No — only Hermes imports isPrivateHostname.
Simplification opportunities: 3 possible cuts, net -20 lines possible

These are safe simplification checks only. Do not remove validation, security controls, data-loss prevention, or required tests.

  • PRA-11 shrink (src/lib/policy/index.ts:1025): Duplicate validation logic in loadPresetFromFile (lines 1025-1037)
    • Replacement: Call shared validatePresetNoAllowedIps(parsed, presetName, true) from loadPresetFromFile and validatePresetNoAllowedIps(parsed, presetName, false) from loadPreset
    • Net: -10 lines
    • Safety boundary: Must preserve current behavior: user presets rejected on any allowed_ips; built-in presets accepted but audited
  • PRA-12 stdlib (src/lib/onboard/inference-providers/hermes.test.ts:9): Inline mock implementation of isPrivateHostname (lines 9-22)
    • Replacement: Import real isPrivateHostname from '../../private-networks' with test fixture YAML, or add parity test for mock
    • Net: -15 lines
    • Safety boundary: Must preserve test isolation — mock should not depend on file system in unit tests
  • PRA-13 native (src/lib/onboard/inference-providers/remote.ts:56): N/A — additive fix
    • Replacement: Import isPrivateHostname from '../../private-networks' and add check before upsertProvider for compatible-endpoint
    • Net: 5 lines
    • Safety boundary: Must not break existing compatible-endpoint onboarding for public endpoints
Test follow-ups to resolve or justify

If these cover changed behavior, prefer adding them in this PR; otherwise state why existing coverage is enough or link the follow-up.

  • PRA-T1 Runtime validation — Integration test: OpenShell sandbox with --security-opt=no-new-privileges running nemoclaw exec and verifying .openclaw perms post-exec. Unit tests cover changed logic well. Runtime/sandbox/infrastructure paths need behavioral runtime validation: scripts/nemoclaw-start.sh (real container perms), hermes.ts (E2E onboard with malicious URL), policy/index.ts (E2E policy add with malicious preset).
  • PRA-T2 Runtime validation — E2E test: nemoclaw onboard --agent hermes with malicious endpointUrl (http://169.254.169.254/\) should fail with 'private or internal address' error. Unit tests cover changed logic well. Runtime/sandbox/infrastructure paths need behavioral runtime validation: scripts/nemoclaw-start.sh (real container perms), hermes.ts (E2E onboard with malicious URL), policy/index.ts (E2E policy add with malicious preset).
  • PRA-T3 Runtime validation — E2E test: nemoclaw policy add with a preset containing allowed_ips should be rejected at load time. Unit tests cover changed logic well. Runtime/sandbox/infrastructure paths need behavioral runtime validation: scripts/nemoclaw-start.sh (real container perms), hermes.ts (E2E onboard with malicious URL), policy/index.ts (E2E policy add with malicious preset).
  • PRA-T4 Runtime validation — E2E test: nemoclaw onboard with compatible-endpoint and malicious endpointUrl should fail at onboarding, not at inference time. Unit tests cover changed logic well. Runtime/sandbox/infrastructure paths need behavioral runtime validation: scripts/nemoclaw-start.sh (real container perms), hermes.ts (E2E onboard with malicious URL), policy/index.ts (E2E policy add with malicious preset).
  • PRA-T5 Runtime validation — Test that null endpointUrl flows through to a safe public default, not a private address. Unit tests cover changed logic well. Runtime/sandbox/infrastructure paths need behavioral runtime validation: scripts/nemoclaw-start.sh (real container perms), hermes.ts (E2E onboard with malicious URL), policy/index.ts (E2E policy add with malicious preset).
  • PRA-T6 Test couples to script formatting via string slicing — Acceptable for this PR. Consider extracting command-execution logic into a sourced shell function for independent unit testing in future.
  • PRA-T7 Permission restore after exec (normalize_mutable_config_perms) — test/nemoclaw-start-perms.test.ts asserts ORDER:cmd → ORDER:normalize + exit code preserved. normalize_mutable_config_perms called at lines 3983 and 4152 in nemoclaw-start.sh after command execution
  • PRA-T8 Null endpointUrl skips SSRF check — hermes.test.ts 'skips SSRF check when endpointUrl is null' expects proceed. Need test that null flows to safe public default.. hermes.ts:32 guards with if (endpointUrl). onboard-probes.ts:930 shows INFERENCE_ROUTE_URL fallback.
Since last review details

Current findings, using the urgency labels above:

PRA-1 Resolve/justify — Source-of-truth review needed: Permission restore after exec (normalize_mutable_config_perms)

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: test/nemoclaw-start-perms.test.ts asserts ORDER:cmd → ORDER:normalize + exit code preserved
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: normalize_mutable_config_perms called at lines 3983 and 4152 in nemoclaw-start.sh after command execution

PRA-2 Required — Non-root entrypoint: permission restore after exec preserves exit code

  • Location: scripts/nemoclaw-start.sh:3995
  • Category: security
  • Problem: Fixed [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047: non-root path now runs normalize_mutable_config_perms after NEMOCLAW_CMD and preserves exit code. Previously exec replaced the shell, skipping the restore and leaving .openclaw world-writable.
  • Impact: Without this fix, nemoclaw exec in non-root containers (--security-opt=no-new-privileges) collapses .openclaw permissions, enabling sandbox escape via world-writable config.
  • Required action: Fix verified in diff. Run test/nemoclaw-start-perms.test.ts to confirm. Integration test in real sandbox would strengthen confidence.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run test/nemoclaw-start-perms.test.ts — extracts non-root cmd block from real script and asserts ORDER:cmd then ORDER:normalize and exit code 42 preserved.
  • Missing regression test: Integration test in OpenShell sandbox with --security-opt=no-new-privileges running nemoclaw exec and verifying .openclaw perms post-exec.
  • Done when: The required change is committed and verification passes: Run test/nemoclaw-start-perms.test.ts — extracts non-root cmd block from real script and asserts ORDER:cmd then ORDER:normalize and exit code 42 preserved.
  • Evidence: Diff shows exec replaced with command execution + normalize_mutable_config_perms + exit $_nemoclaw_cmd_rc. Test extracts shell block and asserts order and exit code.

PRA-3 Required — Root/step-down entrypoint: permission restore after exec preserves exit code

  • Location: scripts/nemoclaw-start.sh:4115
  • Category: security
  • Problem: Fixed [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047: root path (setpriv/gosu step-down) now runs normalize_mutable_config_perms after NEMOCLAW_CMD via STEP_DOWN_PREFIX_SANDBOX and preserves exit code. Same fix as non-root path for privileged containers.
  • Impact: Without this fix, nemoclaw exec in root-mode containers leaves .openclaw permissions mutated after arbitrary user commands run as sandbox user.
  • Required action: Fix verified in diff. Run test/nemoclaw-start-perms.test.ts second test case.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run test/nemoclaw-start-perms.test.ts second test case — extracts root cmd block and asserts ORDER:cmd then ORDER:normalize with exit code 42.
  • Missing regression test: Integration test in root-mode container (no --security-opt=no-new-privileges) running nemoclaw exec and verifying .openclaw perms post-exec.
  • Done when: The required change is committed and verification passes: Run test/nemoclaw-start-perms.test.ts second test case — extracts root cmd block and asserts ORDER:cmd then ORDER:normalize with exit code 42.
  • Evidence: Diff shows same pattern: command execution via STEP_DOWN_PREFIX_SANDBOX, then normalize_mutable_config_perms, then exit with captured code.

PRA-4 Required — Hermes onboarding SSRF guard validates endpointUrl at trust boundary

  • Location: src/lib/onboard/inference-providers/hermes.ts:35
  • Category: security
  • Problem: Added SSRF validation for Hermes provider endpointUrl using shared isPrivateHostname from private-networks. Rejects loopback, RFC1918, cloud metadata (169.254.169.254), .internal TLD, localhost, and all ranges in nemoclaw-blueprint/private-networks.yaml. Closes gap where URL was validated only at inference time (after credentials dispatched).
  • Impact: Prevents user from pointing Hermes onboarding at internal addresses (cloud metadata, loopback, private ranges) which could leak credentials or enable SSRF before plugin-side validation fires.
  • Required action: Implementation correct — uses canonical block list. Ensure blueprint YAML stays in sync (parity test enforces this).
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run src/lib/onboard/inference-providers/hermes.test.ts — 8 cases cover loopback, cloud metadata, RFC1918, localhost, .internal, malformed URL, public HTTPS, and null endpointUrl skip.
  • Missing regression test: E2E test: nemoclaw onboard --agent hermes with malicious endpointUrl should fail with 'private or internal address' error.
  • Done when: The required change is committed and verification passes: Run src/lib/onboard/inference-providers/hermes.test.ts — 8 cases cover loopback, cloud metadata, RFC1918, localhost, .internal, malformed URL, public HTTPS, and null endpointUrl skip.
  • Evidence: hermes.ts:35-43 adds URL parsing + isPrivateHostname check. hermes.test.ts 8 cases cover all attack vectors.

PRA-5 Required — Policy preset load rejects allowed_ips in user-supplied presets

  • Location: src/lib/policy/index.ts:1025
  • Category: security
  • Problem: Added validation in loadPresetFromFile iterating all network_policies endpoints and returning null with descriptive error if allowed_ips field present. Prevents policy bypass where custom preset could widen egress beyond operator intent (issue security(policy): allowed_ips in user-supplied policy presets not validated before applying to OpenShell #6073).
  • Impact: Blocks user-supplied presets from expanding OpenShell's private-IP allowlist (currently only 10.200.0.2 for gateway dial-back). Without this, a preset with allowed_ips: [10.0.0.0/8] would grant access to entire private networks.
  • Required action: Guard correctly placed at preset load boundary. Audit built-in presets separately to confirm none declare allowed_ips inappropriately (see PRA-7).
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run src/lib/policy/preset-allowed-ips.test.ts — rejects preset with allowed_ips in first policy, rejects in second policy, accepts valid preset without allowed_ips, accepts endpoints omitting allowed_ips entirely.
  • Missing regression test: E2E test: nemoclaw policy add with a preset containing allowed_ips should be rejected at load time.
  • Done when: The required change is committed and verification passes: Run src/lib/policy/preset-allowed-ips.test.ts — rejects preset with allowed_ips in first policy, rejects in second policy, accepts valid preset without allowed_ips, accepts endpoints omitting allowed_ips entirely.
  • Evidence: policy/index.ts:1025-1037 iterates network_policies endpoints and rejects on allowed_ips. preset-allowed-ips.test.ts 4 cases cover rejection/acceptance.

PRA-6 Required — Built-in presets declare allowed_ips with broad RFC1918 ranges — guard only applies to user presets

  • Location: nemoclaw-blueprint/policies/presets/*.yaml
  • Category: security
  • Problem: Found 8 built-in presets with allowed_ips: local-inference, nous-web, nous-browser, nous-code, nous-image, nous-audio, openclaw-diagnostics-otel-local. All use host.openshell.internal with allowed_ips: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. This widens egress beyond default single-IP allowlist (10.200.0.2 for gateway dial-back). While legitimate for local inference/browser/diagnostics, the disparity between built-in and user preset validation creates a trust boundary inconsistency.
  • Impact: Built-in presets bypass the new validation in loadPresetFromFile which only checks user-supplied presets. An attacker who can influence built-in preset selection (unlikely) or a supply-chain compromise of a built-in preset could widen egress. More importantly, the security model is inconsistent: user presets are blocked from allowed_ips but built-in presets with broad RFC1918 ranges are allowed.
  • Required action: Audit each built-in preset: confirm allowed_ips is necessary and minimally scoped. Add explicit allowlist comment in each preset YAML explaining why RFC1918 is needed for host.openshell.internal. Consider extending validation to built-in presets with an allowlist of approved preset names.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Run: grep -r allowed_ips nemoclaw-blueprint/policies/presets/ — shows 8 presets with RFC1918 ranges.
  • Missing regression test: Test that built-in preset allowed_ips entries are reviewed and documented with purpose field.
  • Done when: The required change is committed and verification passes: Run: grep -r allowed_ips nemoclaw-blueprint/policies/presets/ — shows 8 presets with RFC1918 ranges.
  • Evidence: local-inference.yaml, nous-web.yaml, nous-browser.yaml, nous-code.yaml, nous-image.yaml, nous-audio.yaml, openclaw-diagnostics-otel-local.yaml all declare allowed_ips with RFC1918 ranges for host.openshell.internal endpoints.

PRA-7 Resolve/justify — Null endpointUrl skips SSRF check — verify downstream safety

  • Location: src/lib/onboard/inference-providers/hermes.ts:39
  • Category: security
  • Problem: When endpointUrl is null, the guard is bypassed (if (endpointUrl) { ... }) and onboarding proceeds. Downstream consumers of Hermes provider config must handle null endpointUrl safely (not default to localhost/private). verifyOnboardInferenceSmoke receives null and falls back to INFERENCE_ROUTE_URL (https://inference.local/v1 — resolves to private via host.openshell.internal).
  • Impact: If null endpointUrl results in a private/default endpoint being configured in the gateway, the SSRF guard is effectively bypassed for the default case.
  • Recommended action: Verify downstream code paths handle null endpointUrl without defaulting to private addresses. Add comment documenting null-skip rationale (e.g., 'null means use default public endpoint configured elsewhere').
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check callers of setupHermesProviderInference and downstream inference setup — ensure null endpointUrl doesn't resolve to localhost/private IP. Trace: hermes.ts:39 skips check → deps.runOpenshell called with inference set command → verifyInferenceRoute/verifyOnboardInferenceSmoke called with null endpointUrl → falls back to INFERENCE_ROUTE_URL.
  • Missing regression test: Test that null endpointUrl flows through to a safe public default, not a private address.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check callers of setupHermesProviderInference and downstream inference setup — ensure null endpointUrl doesn't resolve to localhost/private IP. Trace: hermes.ts:39 skips check → deps.runOpenshell called with inference set command → verifyInferenceRoute/verifyOnboardInferenceSmoke called with null endpointUrl → falls back to INFERENCE_ROUTE_URL.
  • Evidence: hermes.ts:32 guards with if (endpointUrl). hermes.test.ts:177-188 explicitly tests and expects null to skip check and proceed. onboard-probes.ts:930 shows INFERENCE_ROUTE_URL = https://inference.local/v1 as fallback.

PRA-8 Resolve/justify — Remote providers lack onboarding-time SSRF validation

  • Location: src/lib/onboard/inference-providers/remote.ts:56
  • Category: security
  • Problem: Only Hermes provider has onboarding SSRF check. setupRemoteProviderInference (used by nvidia-nim, openai, anthropic, gemini, compatible-endpoint, bedrock) passes endpointUrl through unvalidated to upsertProvider. Plugin-side validateEndpointUrl catches at inference time with DNS pinning, but after URL is already persisted.
  • Impact: User could supply private endpoint during onboarding for remote providers. Credentials could be dispatched to internal host before plugin validation fires. Consistent trust boundary requires onboarding validation for all providers.
  • Recommended action: Add isPrivateHostname check in setupRemoteProviderInference before upsertProvider, matching Hermes pattern. For compatible-endpoint specifically, consider full validateEndpointUrl with DNS pinning since endpoint is user-controlled.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Trace remote.ts:56-67 — resolvedEndpointUrl = endpointUrl || config.endpointUrl passed to upsertProvider without validation.
  • Missing regression test: E2E test: nemoclaw onboard with compatible-endpoint and malicious endpointUrl should fail at onboarding, not at inference time.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Trace remote.ts:56-67 — resolvedEndpointUrl = endpointUrl || config.endpointUrl passed to upsertProvider without validation.
  • Evidence: remote.ts has no isPrivateHostname import or call. Hermes fix is in hermes.ts only.

PRA-9 Resolve/justify — Test couples to script formatting via string slicing

  • Location: test/nemoclaw-start-perms.test.ts:25
  • Category: tests
  • Problem: Module-scope helpers (nonRootCmdBlock, rootCmdBlock) slice the real script by exact string matching (markers, whitespace). Pragmatic but fragile — if nemoclaw-start.sh is reformatted or refactored, tests may break despite unchanged behavior.
  • Impact: Test maintenance burden; false negatives if script formatting changes. Acceptable for this PR but consider extraction for long-term stability.
  • Recommended action: Acceptable for this PR. Consider extracting command-execution logic into a sourced shell function for independent unit testing in future.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect test helpers at lines 14-34 — they use indexOf/slice on raw script text with exact marker strings like '# ── Non-root fallback' and ' if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then\n'.
  • Missing regression test: N/A — architectural improvement, not a bug. Future: refactor test to use behavioral contract (sourced shell library) rather than string slicing.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect test helpers at lines 14-34 — they use indexOf/slice on raw script text with exact marker strings like '# ── Non-root fallback' and ' if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then\n'.
  • Evidence: Test reads START_SCRIPT, slices by markers, injects into bash -c. Breaks if whitespace or comments change.

PRA-10 Resolve/justify — Document permission restore workaround rationale

  • Location: scripts/nemoclaw-start.sh:3995
  • Category: architecture
  • Problem: The normalize_mutable_config_perms call after exec is a workaround for exec mutating .openclaw permissions. No code comment explains the pattern or its rationale near the call site.
  • Impact: Future maintainers may not understand why normalization runs after command execution, potentially removing it and reintroducing [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047.
  • Recommended action: Add code comment near normalize_mutable_config_perms call in both non-root (line 3995) and root (line 4115) paths explaining: why permissions get mutated during exec, why restore runs after, and that this is a workaround for [DGX Spark][Sandbox] openclaw doctor --fix collapses /sandbox/.openclaw permissions, breaking gateway group-writable contract #6047.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Check lines around 3995 and 4115 in nemoclaw-start.sh for comments about permission restore.
  • Missing regression test: N/A — documentation improvement.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Check lines around 3995 and 4115 in nemoclaw-start.sh for comments about permission restore.
  • Evidence: normalize_mutable_config_perms called at lines 3983 and 4152 with no explanatory comment nearby.

PRA-11 Improvement — Extract shared allowed_ips validation for user and built-in preset paths

  • Location: src/lib/policy/index.ts:1025
  • Category: correctness
  • Problem: allowed_ips validation only in loadPresetFromFile (user presets), not in loadPreset (built-in presets). Two code paths with different validation. Intentional (trusted vs untrusted) but creates maintenance risk.
  • Impact: Inconsistent validation logic; built-in preset audit requires manual grep. Shared helper would improve auditability and prevent drift.
  • Suggested action: Extract validation logic to shared helper validatePresetNoAllowedIps(presetContent, presetName, isUserSupplied) for consistency. Call from both loadPresetFromFile and loadPreset (with isUserSupplied flag controlling error vs allow behavior for built-ins).
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Compare loadPresetFromFile (lines 1025-1037) vs loadPreset (lines 80-90) — no shared validation.
  • Missing regression test: Shared validation function tested for both user and built-in preset paths.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: loadPresetFromFile iterates np = parsed.network_policies and rejects on allowed_ips. loadPreset only reads file content, no validation.

PRA-12 Improvement — Test mock for isPrivateHostname duplicates canonical logic and may drift

  • Location: src/lib/onboard/inference-providers/hermes.test.ts:9
  • Category: correctness
  • Problem: Test mock reimplements private hostname detection (localhost, host.docker.internal, RFC1918 patterns, .internal/.local TLDs) instead of using real implementation. Parity test exists but mock could diverge from private-networks.yaml.
  • Impact: If canonical block list changes (new CIDR or name entry), test mock won't reflect it, giving false confidence.
  • Suggested action: Use real isPrivateHostname implementation in tests with a test-specific private-networks.yaml fixture, or add test that verifies mock classification matches canonical implementation for all entries in private-networks.yaml.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Compare mock at lines 9-22 against private-networks.yaml entries and private-networks.ts isPrivateHostname.
  • Missing regression test: Test that mock isPrivateHostname classification matches canonical implementation for all entries in private-networks.yaml.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: Mock at lines 9-22 defines privateHosts Set and privatePatterns regexes. Canonical implementation in private-networks.ts loads from YAML and uses BlockList.

PRA-13 Improvement — Add onboarding-time SSRF check for compatible-endpoint provider

  • Location: src/lib/onboard/inference-providers/remote.ts:56
  • Category: security
  • Problem: compatible-endpoint allows fully user-controlled endpointUrl. Currently no SSRF validation at onboarding — only plugin-side validateEndpointUrl at inference time. This is the highest-risk provider for SSRF.
  • Impact: User can point compatible-endpoint at internal hosts during onboarding. Credentials dispatched before plugin validation.
  • Suggested action: Add isPrivateHostname check in setupRemoteProviderInference for compatible-endpoint specifically, or for all remote providers. Consider full validateEndpointUrl with DNS pinning for compatible-endpoint since endpoint is entirely user-controlled.
  • Expected follow-up: Prefer a current-PR fix when local to changed code; defer only with rationale or linked follow-up.
  • Verification: Trace remote.ts:102 — compatible-endpoint branch uses LOCAL_INFERENCE_TIMEOUT_SECS but no SSRF check on resolvedEndpointUrl.
  • Missing regression test: E2E test: nemoclaw onboard with compatible-endpoint and http://169.254.169.254/ should fail at onboarding.
  • Done when: The local improvement is applied, or the PR notes why it should be deferred.
  • Evidence: remote.ts imports private-networks functions? No — only Hermes imports isPrivateHostname.

Workflow run details

This is an automated, non-binding review; it still expects maintainers and agents to respond to each required or warning item. Treat suggestions as current-PR improvements when they touch changed code; defer only with maintainer rationale or a linked follow-up. A human maintainer must make the final merge decision.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

E2E Advisor Recommendation

Required E2E: hermes-e2e, network-policy, sandbox-operations, test-e2e-gateway-isolation
Optional E2E: cloud-onboard, inference-routing, onboard-negative-paths

Dispatch hint: hermes-e2e,network-policy,sandbox-operations

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • hermes-e2e (high): Exercises the real Hermes onboarding and hosted inference flow after the Hermes Provider endpoint validation change, ensuring public hosted endpoints still configure credentials, provider routing, and live inference successfully.
  • network-policy (high): Required for the policy loader change because it validates live policy-add/list behavior, network policy allow/deny enforcement, and related SSRF/private-address policy boundaries against a real sandbox.
  • sandbox-operations (high): Covers live sandbox lifecycle and real nemoclaw <sandbox> agent command paths that invoke nemoclaw-start <command> inside a running sandbox, which is directly adjacent to the changed command-mode cleanup and permission normalization logic.
  • test-e2e-gateway-isolation (medium): Image-level entrypoint/security-boundary coverage for scripts/nemoclaw-start.sh, including root/non-root entrypoint hardening and non-root command execution behavior in the built sandbox image.

Optional E2E

  • cloud-onboard (high): Useful broad confidence for the full hosted OpenClaw onboarding path after touching the shared sandbox entrypoint and onboarding/inference-adjacent code, but sandbox-operations and hermes-e2e provide the more directly targeted required coverage.
  • inference-routing (high): Adjacent confidence for hosted inference routing after an inference provider setup change, especially if reviewers want non-Hermes routing coverage in addition to hermes-e2e.
  • onboard-negative-paths (medium): Adjacent negative-path onboarding coverage for fail-closed onboarding behavior; useful but not directly scoped to the new Hermes private-endpoint rejection unless that case is added to the suite.

New E2E recommendations

  • Hermes inference endpoint SSRF guard (high): Existing live Hermes coverage verifies a valid public endpoint path, but does not appear to assert that nemoclaw onboard --agent hermes or Hermes provider setup fails closed for loopback, metadata, RFC1918, or malformed endpoint URLs without preparing credentials or mutating provider state.
    • Suggested test: Add a Hermes negative-path live E2E that attempts onboarding/provider setup with private/internal endpoint URLs and asserts a non-zero/fail-closed result plus no provider-store or credential side effects.
  • custom network policy preset allowed_ips rejection (high): The unit test covers loadPresetFromFile, but live E2E should prove that nemoclaw <sandbox> policy-add <custom.yaml> rejects user-supplied allowed_ips and does not apply or persist the custom preset to the sandbox registry/gateway policy.
    • Suggested test: Extend network-policy E2E with a custom preset fixture containing allowed_ips, then assert policy-add fails, policy-list does not include the preset, and the gateway policy remains unchanged.
  • nemoclaw-start command-mode permission restoration (medium): The new unit test verifies extracted shell blocks, but a live sandbox should validate that real nemoclaw-start <command> invocations leave /sandbox/.openclaw writable and preserve command exit status under the actual image/user/permission model.
    • Suggested test: Add a live sandbox command-mode regression that runs an in-sandbox nemoclaw-start one-shot command, checks the exit code, then verifies .openclaw directory/config/hash permissions and a subsequent OpenClaw state write still succeed.

Dispatch hint

  • Workflow: .github/workflows/e2e.yaml
  • jobs input: hermes-e2e,network-policy,sandbox-operations

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

E2E Target Recommendation

Required E2E targets: network-policy
Optional E2E targets: hermes-inference-switch

Dispatch required E2E targets:

  • gh workflow run e2e.yaml --ref <pr-head-ref> --field jobs=network-policy

Workflow run

Full E2E target advisor summary

E2E Target Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E targets

  • network-policy: The PR changes user-supplied policy preset validation and the shared nemoclaw-start command path. The network-policy live job exercises policy-add from preset files, live policy allow/deny behavior, and an in-sandbox nemoclaw-start command invocation, making it the smallest wired live target for these surfaces.
    • Dispatch: gh workflow run e2e.yaml --ref <pr-head-ref> --field jobs=network-policy

Optional E2E targets

  • hermes-inference-switch: Optional adjacent Hermes coverage for endpoint URL handling during an inference-route switch with live runtime probes.
    • Dispatch: gh workflow run e2e.yaml --ref <pr-head-ref> --field jobs=hermes-inference-switch

Relevant changed files

  • scripts/nemoclaw-start.sh
  • src/lib/onboard/inference-providers/hermes.ts
  • src/lib/policy/index.ts

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Blocked

Merge posture: Do not merge until addressed
Primary next action: Fix PRA-3: Use full Hermes endpoint SSRF validation before persistence; then add or justify PRA-T1.
Open items: 1 required · 6 warnings · 0 suggestions · 8 test follow-ups
Since last review: 0 prior items resolved · 7 still apply · 0 new items found

Action checklist

  • PRA-3 Fix: Use full Hermes endpoint SSRF validation before persistence in src/lib/onboard/inference-providers/hermes.ts:32
  • PRA-1 Resolve or justify: Source-of-truth review needed: Hermes endpoint URL validation
  • PRA-2 Resolve or justify: Source-of-truth review needed: nemoclaw-start one-shot command permission restoration
  • PRA-4 Resolve or justify: Avoid echoing raw endpoint URLs in validation errors in src/lib/onboard/inference-providers/hermes.ts:37
  • PRA-5 Resolve or justify: Preserve one-shot command signal semantics after replacing exec in scripts/nemoclaw-start.sh:3952
  • PRA-6 Resolve or justify: Use the production private-network matcher in Hermes SSRF tests in src/lib/onboard/inference-providers/hermes.test.ts:8
  • PRA-7 Resolve or justify: Connect the one-shot permission restoration change to its acceptance context in scripts/nemoclaw-start.sh:3952
  • PRA-T1 Add or justify test follow-up: Runtime validation
  • PRA-T2 Add or justify test follow-up: Runtime validation
  • PRA-T3 Add or justify test follow-up: Runtime validation
  • PRA-T4 Add or justify test follow-up: Runtime validation
  • PRA-T5 Add or justify test follow-up: Runtime validation
  • PRA-T6 Add or justify test follow-up: Use the production private-network matcher in Hermes SSRF tests
  • PRA-T7 Add or justify test follow-up: Acceptance clause
  • PRA-T8 Add or justify test follow-up: Acceptance clause

Findings index

ID Severity Category Location Required action
PRA-1 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-2 Resolve/justify architecture Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
PRA-3 Required security src/lib/onboard/inference-providers/hermes.ts:32 Validate endpointUrl with a shared source-of-truth endpoint validator before passing baseUrl downstream, or implement equivalent validation here: trim and parse, require http: or https:, require a hostname, reject private/reserved hostnames and IP literals, resolve DNS answers for names, reject private/reserved answers with isPrivateIp, and preserve any required HTTP DNS-pinning semantics.
PRA-4 Resolve/justify security src/lib/onboard/inference-providers/hermes.ts:37 Report malformed endpoint failures without including the full raw URL, or pass the value through a URL redaction helper that removes userinfo and query/fragment data before formatting the error.
PRA-5 Resolve/justify correctness scripts/nemoclaw-start.sh:3952 Add explicit signal forwarding/wait handling for the one-shot child in both non-root and root step-down branches, or document and test why Bash/PID 1 behavior is sufficient while still guaranteeing post-command permission normalization.
PRA-6 Resolve/justify tests src/lib/onboard/inference-providers/hermes.test.ts:8 Remove the private-networks mock unless a specific dependency-injection seam is needed for DNS lookup; use the real module for literal/name classification and mock only network/DNS resolution if full endpoint validation adds DNS checks.
PRA-7 Resolve/justify scope scripts/nemoclaw-start.sh:3952 Add the relevant #6047 acceptance context to the PR description or otherwise make the script change's scope explicit in this PR, including the intended behavior for permissions, exit codes, and signals.

🚨 Required before merge

Address these before merging unless a maintainer explicitly overrides the advisor with rationale.

PRA-3 Required — Use full Hermes endpoint SSRF validation before persistence

  • Location: src/lib/onboard/inference-providers/hermes.ts:32
  • Category: security
  • Problem: The new Hermes guard parses endpointUrl and checks only parsedEndpoint.hostname with isPrivateHostname(). That blocks some literal private hosts, but it does not require http: or https:, does not reuse the existing validateEndpointUrl-style source of truth, and does not resolve public-looking hostnames to reject private, link-local, loopback, ULA, or metadata DNS answers before passing endpointUrl as baseUrl to Hermes credential setup.
  • Impact: A user-controlled Hermes endpoint can still be persisted as OpenShell provider configuration and may dispatch inference credentials to an unsupported scheme or an internal/private destination through DNS indirection, leaving the security(onboarding): Hermes inference endpoint URL skips SSRF validation before being persisted #6072 SSRF boundary only partially fixed.
  • Required action: Validate endpointUrl with a shared source-of-truth endpoint validator before passing baseUrl downstream, or implement equivalent validation here: trim and parse, require http: or https:, require a hostname, reject private/reserved hostnames and IP literals, resolve DNS answers for names, reject private/reserved answers with isPrivateIp, and preserve any required HTTP DNS-pinning semantics.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Read src/lib/onboard/inference-providers/hermes.ts around the endpointUrl guard and compare it with nemoclaw/src/blueprint/ssrf.ts validateEndpointUrl() or src/lib/sandbox/config.ts validateUrlValueWithDnsResult(); confirm Hermes now performs scheme, hostname, and DNS-answer validation before ensureHermesProviderApiKeyCredentials/ensureHermesProviderOAuthCredentials receive baseUrl.
  • Missing regression test: Add behavior tests named `setupHermesProviderInference rejects non-http endpoint schemes before provider registration`, `setupHermesProviderInference rejects endpoint URLs with no hostname before provider registration`, `setupHermesProviderInference rejects IPv6 loopback and ULA endpoint literals before provider registration`, and `setupHermesProviderInference rejects hostnames whose DNS answers are private before provider registration`; assert the Hermes credential preparation mocks are not called on each rejection.
  • Done when: The required change is committed and verification passes: Read src/lib/onboard/inference-providers/hermes.ts around the endpointUrl guard and compare it with nemoclaw/src/blueprint/ssrf.ts validateEndpointUrl() or src/lib/sandbox/config.ts validateUrlValueWithDnsResult(); confirm Hermes now performs scheme, hostname, and DNS-answer validation before ensureHermesProviderApiKeyCredentials/ensureHermesProviderOAuthCredentials receive baseUrl.
  • Evidence: The diff adds `parsedEndpoint = new URL(endpointUrl)` followed by `if (isPrivateHostname(parsedEndpoint.hostname)) throw ...`; the changed tests cover 127.0.0.1, 169.254.169.254, 10.0.0.1, localhost, .internal, malformed input, public HTTPS, and null, but not scheme rejection, DNS-to-private rejection, or IPv6 private literals with the production matcher.
Review findings by urgency: 1 required fix, 6 items to resolve/justify, 0 in-scope improvements

⚠️ Resolve or justify before merge

Investigate these in the current review; either fix them, explain why they are not applicable, or document the accepted risk.

PRA-1 Resolve/justify — Source-of-truth review needed: Hermes endpoint URL validation

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: Existing tests cover some literal private IPv4 and reserved-name cases only; missing tests should cover non-http schemes, missing-host URL forms, IPv6 private literals, DNS answers resolving to private IPs, provider-registration non-call assertions, and redacted errors.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: hermes.ts imports isPrivateHostname from ../../private-networks and checks only parsedEndpoint.hostname.

PRA-2 Resolve/justify — Source-of-truth review needed: nemoclaw-start one-shot command permission restoration

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: test/nemoclaw-start-perms.test.ts proves cleanup ordering and exit-code preservation in sliced shell harnesses; missing coverage should prove SIGTERM/SIGINT behavior for an in-flight one-shot command.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: The diff replaces exec with child invocation, rc capture, normalize_mutable_config_perms, and exit in both NEMOCLAW_CMD branches.

PRA-4 Resolve/justify — Avoid echoing raw endpoint URLs in validation errors

  • Location: src/lib/onboard/inference-providers/hermes.ts:37
  • Category: security
  • Problem: The malformed URL path throws `Invalid inference endpoint URL: ${endpointUrl}`. Endpoint URLs are user supplied and may include userinfo or query-string tokens copied from provider dashboards or examples.
  • Impact: Credential-bearing endpoint strings can be written to CLI output, logs, or automation artifacts, creating an avoidable secret disclosure path.
  • Recommended action: Report malformed endpoint failures without including the full raw URL, or pass the value through a URL redaction helper that removes userinfo and query/fragment data before formatting the error.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Read the catch block in src/lib/onboard/inference-providers/hermes.ts around the `new URL(endpointUrl)` call and confirm the thrown message no longer interpolates unredacted endpointUrl.
  • Missing regression test: Add `setupHermesProviderInference redacts credential-bearing malformed endpoint URLs in errors`, using an endpoint string containing userinfo and a token query parameter and asserting the thrown message omits both secret values.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Read the catch block in src/lib/onboard/inference-providers/hermes.ts around the `new URL(endpointUrl)` call and confirm the thrown message no longer interpolates unredacted endpointUrl.
  • Evidence: The catch block currently does `throw new Error(`Invalid inference endpoint URL: ${endpointUrl}`)` with no redaction.

PRA-5 Resolve/justify — Preserve one-shot command signal semantics after replacing exec

  • Location: scripts/nemoclaw-start.sh:3952
  • Category: correctness
  • Problem: The entrypoint previously used `exec` for one-shot NEMOCLAW_CMD paths. The PR now launches the command as a foreground child, captures its exit code, normalizes permissions, and exits. That enables cleanup, but it also changes PID and signal behavior without explicit signal forwarding or a runtime-style signal test.
  • Impact: A SIGTERM or SIGINT delivered to the entrypoint while a long-running one-shot command is active may not reach the child with the same semantics as the former exec path, causing hung shutdowns, orphaned work, or skipped cleanup behavior in sandbox lifecycle paths.
  • Recommended action: Add explicit signal forwarding/wait handling for the one-shot child in both non-root and root step-down branches, or document and test why Bash/PID 1 behavior is sufficient while still guaranteeing post-command permission normalization.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect both NEMOCLAW_CMD branches in scripts/nemoclaw-start.sh and confirm either traps forward SIGTERM/SIGINT to the active child and wait for it before normalizing, or a targeted test demonstrates the current behavior for an in-flight long-running command.
  • Missing regression test: Add `nemoclaw-start forwards SIGTERM to an active non-root NEMOCLAW_CMD before normalizing perms` and `nemoclaw-start forwards SIGTERM to an active root step-down NEMOCLAW_CMD before normalizing perms`; the tests should use a long-running child that records receipt of the signal and assert the parent exits predictably after cleanup.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect both NEMOCLAW_CMD branches in scripts/nemoclaw-start.sh and confirm either traps forward SIGTERM/SIGINT to the active child and wait for it before normalizing, or a targeted test demonstrates the current behavior for an in-flight long-running command.
  • Evidence: Both changed branches now run `"${NEMOCLAW_CMD[@]}" || _nemoclaw_cmd_rc=$?` or `"${STEP_DOWN_PREFIX_SANDBOX[@]}" "${NEMOCLAW_CMD[@]}" || _nemoclaw_cmd_rc=$?`, then call `normalize_mutable_config_perms` and `exit $_nemoclaw_cmd_rc`; no new trap or forwarding logic is added. The new tests cover cleanup ordering and exit-code preservation only.

PRA-6 Resolve/justify — Use the production private-network matcher in Hermes SSRF tests

  • Location: src/lib/onboard/inference-providers/hermes.test.ts:8
  • Category: tests
  • Problem: The Hermes tests mock `../../private-networks` with a small local matcher instead of exercising the canonical YAML-backed isPrivateHostname implementation used in production.
  • Impact: The tests can pass while production behavior drifts for IPv6, reserved names, trailing-dot/case normalization, or future private-network YAML changes, reducing confidence in this security boundary.
  • Recommended action: Remove the private-networks mock unless a specific dependency-injection seam is needed for DNS lookup; use the real module for literal/name classification and mock only network/DNS resolution if full endpoint validation adds DNS checks.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Read src/lib/onboard/inference-providers/hermes.test.ts and confirm `vi.mock("../../private-networks", ...)` is removed or limited to a DNS seam, then check that tests still assert private literal/name behavior through the production matcher.
  • Missing regression test: Add `setupHermesProviderInference rejects IPv6 loopback using the production private-network matcher` and `setupHermesProviderInference rejects reserved internal names using the production private-network matcher` after removing the mock.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Read src/lib/onboard/inference-providers/hermes.test.ts and confirm `vi.mock("../../private-networks", ...)` is removed or limited to a DNS seam, then check that tests still assert private literal/name behavior through the production matcher.
  • Evidence: The test file defines its own Set and regexes for localhost, host.docker.internal, RFC1918 IPv4, 169.254, .internal, and .local; it does not load nemoclaw-blueprint/private-networks.yaml.

PRA-7 Resolve/justify — Connect the one-shot permission restoration change to its acceptance context

💡 In-scope improvements

These are lower-risk, not throwaway. Prefer fixing them in this PR when they are local to changed code; defer only with rationale or a linked follow-up.

  • None.
Simplification opportunities: 1 possible cut

These are safe simplification checks only. Do not remove validation, security controls, data-loss prevention, or required tests.

  • PRA-3 shrink (src/lib/onboard/inference-providers/hermes.ts:32): The local Hermes-only subset of endpoint validation.
    • Replacement: Reuse or extract the existing validateEndpointUrl-style validator so one source owns scheme, hostname, private-name, DNS-answer, and pinning behavior.
    • Safety boundary: Do not remove any trust-boundary validation; the replacement must still run before credentials or provider base URLs are persisted.
Test follow-ups to resolve or justify

If these cover changed behavior, prefer adding them in this PR; otherwise state why existing coverage is enough or link the follow-up.

  • PRA-T1 Runtime validation — setupHermesProviderInference rejects non-http endpoint schemes before provider registration. The changed code touches credentials/inference network validation, custom policy loading, and sandbox entrypoint lifecycle behavior. Unit tests were added, but the Hermes SSRF boundary needs source-of-truth/DNS coverage and the entrypoint no-exec change needs signal behavior validation.
  • PRA-T2 Runtime validation — setupHermesProviderInference rejects hostnames whose DNS answers are private before provider registration. The changed code touches credentials/inference network validation, custom policy loading, and sandbox entrypoint lifecycle behavior. Unit tests were added, but the Hermes SSRF boundary needs source-of-truth/DNS coverage and the entrypoint no-exec change needs signal behavior validation.
  • PRA-T3 Runtime validation — setupHermesProviderInference rejects IPv6 loopback and ULA endpoint literals using the production private-network matcher. The changed code touches credentials/inference network validation, custom policy loading, and sandbox entrypoint lifecycle behavior. Unit tests were added, but the Hermes SSRF boundary needs source-of-truth/DNS coverage and the entrypoint no-exec change needs signal behavior validation.
  • PRA-T4 Runtime validation — setupHermesProviderInference redacts credential-bearing malformed endpoint URLs in errors. The changed code touches credentials/inference network validation, custom policy loading, and sandbox entrypoint lifecycle behavior. Unit tests were added, but the Hermes SSRF boundary needs source-of-truth/DNS coverage and the entrypoint no-exec change needs signal behavior validation.
  • PRA-T5 Runtime validation — nemoclaw-start forwards SIGTERM to an active non-root NEMOCLAW_CMD before normalizing perms. The changed code touches credentials/inference network validation, custom policy loading, and sandbox entrypoint lifecycle behavior. Unit tests were added, but the Hermes SSRF boundary needs source-of-truth/DNS coverage and the entrypoint no-exec change needs signal behavior validation.
  • PRA-T6 Use the production private-network matcher in Hermes SSRF tests — Remove the private-networks mock unless a specific dependency-injection seam is needed for DNS lookup; use the real module for literal/name classification and mock only network/DNS resolution if full endpoint validation adds DNS checks.
  • PRA-T7 Acceptance clause — During Hermes provider onboarding, the user-supplied inference endpoint URL (`endpointUrl`) is normalized via `normalizeProviderBaseUrl()` but is **not** validated for SSRF before being persisted and handed to OpenShell as `OPENAI_BASE_URL`. — add test evidence or identify existing coverage. hermes.ts now adds a pre-persistence guard, but it only checks parsedEndpoint.hostname with isPrivateHostname and does not perform the full validateEndpointUrl-style scheme and DNS-answer SSRF checks.
  • PRA-T8 Acceptance clause — 1. User supplies `http://169.254.169.254/\` (or any private/internal host) as the Hermes inference endpoint during interactive onboarding — add test evidence or identify existing coverage. Tests cover 169.254.169.254 plus some private literals and names, but public-looking hostnames resolving to private/internal addresses remain untested and unblocked.
Since last review details

Current findings, using the urgency labels above:

PRA-1 Resolve/justify — Source-of-truth review needed: Hermes endpoint URL validation

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: Existing tests cover some literal private IPv4 and reserved-name cases only; missing tests should cover non-http schemes, missing-host URL forms, IPv6 private literals, DNS answers resolving to private IPs, provider-registration non-call assertions, and redacted errors.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: hermes.ts imports isPrivateHostname from ../../private-networks and checks only parsedEndpoint.hostname.

PRA-2 Resolve/justify — Source-of-truth review needed: nemoclaw-start one-shot command permission restoration

  • Location: not file-specific
  • Category: architecture
  • Problem: The advisor marked localized patch analysis as needs_followup.
  • Impact: A localized workaround can preserve or hide an invalid state when the source boundary is unclear.
  • Recommended action: Identify the invalid state, source boundary, source-fix constraint, regression test, and removal condition before merging the localized behavior.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Missing regression test: test/nemoclaw-start-perms.test.ts proves cleanup ordering and exit-code preservation in sliced shell harnesses; missing coverage should prove SIGTERM/SIGINT behavior for an in-flight one-shot command.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect the localized patch and source-of-truth review fields for a concrete invalid state, source boundary, source-fix constraint, regression test, and removal condition.
  • Evidence: The diff replaces exec with child invocation, rc capture, normalize_mutable_config_perms, and exit in both NEMOCLAW_CMD branches.

PRA-3 Required — Use full Hermes endpoint SSRF validation before persistence

  • Location: src/lib/onboard/inference-providers/hermes.ts:32
  • Category: security
  • Problem: The new Hermes guard parses endpointUrl and checks only parsedEndpoint.hostname with isPrivateHostname(). That blocks some literal private hosts, but it does not require http: or https:, does not reuse the existing validateEndpointUrl-style source of truth, and does not resolve public-looking hostnames to reject private, link-local, loopback, ULA, or metadata DNS answers before passing endpointUrl as baseUrl to Hermes credential setup.
  • Impact: A user-controlled Hermes endpoint can still be persisted as OpenShell provider configuration and may dispatch inference credentials to an unsupported scheme or an internal/private destination through DNS indirection, leaving the security(onboarding): Hermes inference endpoint URL skips SSRF validation before being persisted #6072 SSRF boundary only partially fixed.
  • Required action: Validate endpointUrl with a shared source-of-truth endpoint validator before passing baseUrl downstream, or implement equivalent validation here: trim and parse, require http: or https:, require a hostname, reject private/reserved hostnames and IP literals, resolve DNS answers for names, reject private/reserved answers with isPrivateIp, and preserve any required HTTP DNS-pinning semantics.
  • Expected follow-up: Fix before merge or get explicit maintainer override.
  • Verification: Read src/lib/onboard/inference-providers/hermes.ts around the endpointUrl guard and compare it with nemoclaw/src/blueprint/ssrf.ts validateEndpointUrl() or src/lib/sandbox/config.ts validateUrlValueWithDnsResult(); confirm Hermes now performs scheme, hostname, and DNS-answer validation before ensureHermesProviderApiKeyCredentials/ensureHermesProviderOAuthCredentials receive baseUrl.
  • Missing regression test: Add behavior tests named `setupHermesProviderInference rejects non-http endpoint schemes before provider registration`, `setupHermesProviderInference rejects endpoint URLs with no hostname before provider registration`, `setupHermesProviderInference rejects IPv6 loopback and ULA endpoint literals before provider registration`, and `setupHermesProviderInference rejects hostnames whose DNS answers are private before provider registration`; assert the Hermes credential preparation mocks are not called on each rejection.
  • Done when: The required change is committed and verification passes: Read src/lib/onboard/inference-providers/hermes.ts around the endpointUrl guard and compare it with nemoclaw/src/blueprint/ssrf.ts validateEndpointUrl() or src/lib/sandbox/config.ts validateUrlValueWithDnsResult(); confirm Hermes now performs scheme, hostname, and DNS-answer validation before ensureHermesProviderApiKeyCredentials/ensureHermesProviderOAuthCredentials receive baseUrl.
  • Evidence: The diff adds `parsedEndpoint = new URL(endpointUrl)` followed by `if (isPrivateHostname(parsedEndpoint.hostname)) throw ...`; the changed tests cover 127.0.0.1, 169.254.169.254, 10.0.0.1, localhost, .internal, malformed input, public HTTPS, and null, but not scheme rejection, DNS-to-private rejection, or IPv6 private literals with the production matcher.

PRA-4 Resolve/justify — Avoid echoing raw endpoint URLs in validation errors

  • Location: src/lib/onboard/inference-providers/hermes.ts:37
  • Category: security
  • Problem: The malformed URL path throws `Invalid inference endpoint URL: ${endpointUrl}`. Endpoint URLs are user supplied and may include userinfo or query-string tokens copied from provider dashboards or examples.
  • Impact: Credential-bearing endpoint strings can be written to CLI output, logs, or automation artifacts, creating an avoidable secret disclosure path.
  • Recommended action: Report malformed endpoint failures without including the full raw URL, or pass the value through a URL redaction helper that removes userinfo and query/fragment data before formatting the error.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Read the catch block in src/lib/onboard/inference-providers/hermes.ts around the `new URL(endpointUrl)` call and confirm the thrown message no longer interpolates unredacted endpointUrl.
  • Missing regression test: Add `setupHermesProviderInference redacts credential-bearing malformed endpoint URLs in errors`, using an endpoint string containing userinfo and a token query parameter and asserting the thrown message omits both secret values.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Read the catch block in src/lib/onboard/inference-providers/hermes.ts around the `new URL(endpointUrl)` call and confirm the thrown message no longer interpolates unredacted endpointUrl.
  • Evidence: The catch block currently does `throw new Error(`Invalid inference endpoint URL: ${endpointUrl}`)` with no redaction.

PRA-5 Resolve/justify — Preserve one-shot command signal semantics after replacing exec

  • Location: scripts/nemoclaw-start.sh:3952
  • Category: correctness
  • Problem: The entrypoint previously used `exec` for one-shot NEMOCLAW_CMD paths. The PR now launches the command as a foreground child, captures its exit code, normalizes permissions, and exits. That enables cleanup, but it also changes PID and signal behavior without explicit signal forwarding or a runtime-style signal test.
  • Impact: A SIGTERM or SIGINT delivered to the entrypoint while a long-running one-shot command is active may not reach the child with the same semantics as the former exec path, causing hung shutdowns, orphaned work, or skipped cleanup behavior in sandbox lifecycle paths.
  • Recommended action: Add explicit signal forwarding/wait handling for the one-shot child in both non-root and root step-down branches, or document and test why Bash/PID 1 behavior is sufficient while still guaranteeing post-command permission normalization.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Inspect both NEMOCLAW_CMD branches in scripts/nemoclaw-start.sh and confirm either traps forward SIGTERM/SIGINT to the active child and wait for it before normalizing, or a targeted test demonstrates the current behavior for an in-flight long-running command.
  • Missing regression test: Add `nemoclaw-start forwards SIGTERM to an active non-root NEMOCLAW_CMD before normalizing perms` and `nemoclaw-start forwards SIGTERM to an active root step-down NEMOCLAW_CMD before normalizing perms`; the tests should use a long-running child that records receipt of the signal and assert the parent exits predictably after cleanup.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Inspect both NEMOCLAW_CMD branches in scripts/nemoclaw-start.sh and confirm either traps forward SIGTERM/SIGINT to the active child and wait for it before normalizing, or a targeted test demonstrates the current behavior for an in-flight long-running command.
  • Evidence: Both changed branches now run `"${NEMOCLAW_CMD[@]}" || _nemoclaw_cmd_rc=$?` or `"${STEP_DOWN_PREFIX_SANDBOX[@]}" "${NEMOCLAW_CMD[@]}" || _nemoclaw_cmd_rc=$?`, then call `normalize_mutable_config_perms` and `exit $_nemoclaw_cmd_rc`; no new trap or forwarding logic is added. The new tests cover cleanup ordering and exit-code preservation only.

PRA-6 Resolve/justify — Use the production private-network matcher in Hermes SSRF tests

  • Location: src/lib/onboard/inference-providers/hermes.test.ts:8
  • Category: tests
  • Problem: The Hermes tests mock `../../private-networks` with a small local matcher instead of exercising the canonical YAML-backed isPrivateHostname implementation used in production.
  • Impact: The tests can pass while production behavior drifts for IPv6, reserved names, trailing-dot/case normalization, or future private-network YAML changes, reducing confidence in this security boundary.
  • Recommended action: Remove the private-networks mock unless a specific dependency-injection seam is needed for DNS lookup; use the real module for literal/name classification and mock only network/DNS resolution if full endpoint validation adds DNS checks.
  • Expected follow-up: Resolve in this PR or explain why the risk is acceptable.
  • Verification: Read src/lib/onboard/inference-providers/hermes.test.ts and confirm `vi.mock("../../private-networks", ...)` is removed or limited to a DNS seam, then check that tests still assert private literal/name behavior through the production matcher.
  • Missing regression test: Add `setupHermesProviderInference rejects IPv6 loopback using the production private-network matcher` and `setupHermesProviderInference rejects reserved internal names using the production private-network matcher` after removing the mock.
  • Done when: The risk is fixed or explicitly justified in the PR. Verification: Read src/lib/onboard/inference-providers/hermes.test.ts and confirm `vi.mock("../../private-networks", ...)` is removed or limited to a DNS seam, then check that tests still assert private literal/name behavior through the production matcher.
  • Evidence: The test file defines its own Set and regexes for localhost, host.docker.internal, RFC1918 IPv4, 169.254, .internal, and .local; it does not load nemoclaw-blueprint/private-networks.yaml.

PRA-7 Resolve/justify — Connect the one-shot permission restoration change to its acceptance context

Workflow run details

This is an automated, non-binding review; it still expects maintainers and agents to respond to each required or warning item. Treat suggestions as current-PR improvements when they touch changed code; defer only with maintainer rationale or a linked follow-up. A human maintainer must make the final merge decision.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/nemoclaw-start.sh`:
- Around line 3981-3984: Preserve the wrapped command’s exit status in the
nemoclaw start flow: in the main execution block that captures _nemoclaw_cmd_rc
and then calls normalize_mutable_config_perms, make sure the helper cannot
overwrite or short-circuit the saved status before exit uses it. Update the
logic around "${NEMOCLAW_CMD[@]}" and normalize_mutable_config_perms so the
shell still exits with _nemoclaw_cmd_rc even when set -e is enabled.

In `@src/lib/onboard/inference-providers/hermes.test.ts`:
- Around line 8-22: Remove the hand-rolled `isPrivateHostname` mock from
`hermes.test.ts` and let `setupHermesProviderInference` use the real classifier
from `src/lib/private-networks.ts`; keep the test focused on the provider
behavior, not a copied production algorithm. Update the test fixtures/assertions
to match the real `isPrivateHostname` behavior for loopback, link-local,
RFC-1918, `.internal`, and `.local` hosts, and simplify the `requireValue` fake
by removing the untested throw branch. Use the existing
`setupHermesProviderInference` and `requireValue` test helpers to locate the
changes.

In `@src/lib/onboard/inference-providers/hermes.ts`:
- Around line 32-44: The validation error in the endpoint parsing block
currently echoes the raw user-supplied endpoint string, which can leak secrets.
Update the error path in the `endpointUrl` handling inside `hermes.ts` to redact
the value before throwing, using the existing `deps`/`redact()` pattern already
used elsewhere in this module. Keep the hostname-based private-address error
as-is, but ensure any thrown message that includes `endpointUrl` no longer
exposes credentials or query secrets.

In `@src/lib/policy/index.ts`:
- Around line 1068-1081: In the network_policies validation loop in policy
parsing, malformed policy entries are being skipped via policyVal/endpoints
shape checks instead of rejected, which lets unsafe data bypass the allowed_ips
guard. Update the validation in the parsed.network_policies traversal to fail
closed when a policyKey entry is not a PolicyObject or when endpoints is not an
array, alongside the existing allowed_ips check, so invalid preset entries are
rejected rather than preserved for later merge/apply.
🪄 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: abc8da9c-af2f-4353-9d84-834d1652799d

📥 Commits

Reviewing files that changed from the base of the PR and between e4b9111 and 2698ec2.

📒 Files selected for processing (6)
  • scripts/nemoclaw-start.sh
  • src/lib/onboard/inference-providers/hermes.test.ts
  • src/lib/onboard/inference-providers/hermes.ts
  • src/lib/policy/index.ts
  • src/lib/policy/preset-allowed-ips.test.ts
  • test/nemoclaw-start-perms.test.ts

Comment thread scripts/nemoclaw-start.sh
Comment on lines +3981 to +3984
_nemoclaw_cmd_rc=0
"${NEMOCLAW_CMD[@]}" || _nemoclaw_cmd_rc=$?
normalize_mutable_config_perms
exit $_nemoclaw_cmd_rc

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether set -e is active at script top-level, and whether this script is referenced as a container ENTRYPOINT.
grep -n "^set -" scripts/nemoclaw-start.sh
fd -e Dockerfile -x grep -nHi "nemoclaw-start" {}

Repository: NVIDIA/NemoClaw

Length of output: 174


scripts/nemoclaw-start.sh:3981-3984 — Preserve the captured exit code across the permission-normalization step. With set -e enabled, normalize_mutable_config_perms can abort before exit $_nemoclaw_cmd_rc, so the wrapper can return that helper's status instead of the command's real exit code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/nemoclaw-start.sh` around lines 3981 - 3984, Preserve the wrapped
command’s exit status in the nemoclaw start flow: in the main execution block
that captures _nemoclaw_cmd_rc and then calls normalize_mutable_config_perms,
make sure the helper cannot overwrite or short-circuit the saved status before
exit uses it. Update the logic around "${NEMOCLAW_CMD[@]}" and
normalize_mutable_config_perms so the shell still exits with _nemoclaw_cmd_rc
even when set -e is enabled.

Source: Path instructions

Comment on lines +8 to +22
vi.mock("../../private-networks", () => ({
isPrivateHostname: (hostname: string) => {
const privateHosts = new Set(["localhost", "host.docker.internal"]);
const privatePatterns = [
/^127\./,
/^10\./,
/^192\.168\./,
/^172\.(1[6-9]|2\d|3[01])\./,
/^169\.254\./,
];
if (privateHosts.has(hostname)) return true;
if (hostname.endsWith(".internal") || hostname.endsWith(".local")) return true;
return privatePatterns.some((re) => re.test(hostname));
},
}));

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the hand-rolled isPrivateHostname mock — it copies the production algorithm and is failing CI.

The vi.mock reimplements private-hostname classification (Lines 8-22) instead of exercising the real isPrivateHostname from src/lib/private-networks.ts. That means these tests only prove setupHermesProviderInference reacts correctly to whatever the fake returns — not that the real classifier flags loopback/link-local/RFC-1918/.internal hosts as private. This is exactly the "copied production algorithms, broad mocks that bypass the behavior under test" pattern called out for test files.

This is also the root cause of the CI guardrail failure ("Hermes test file has 3 if statement(s), up from 0"): 2 of the 3 come from this mock (Lines 18-19), the 3rd from the requireValue fake's untested branch (Line 51).

Dropping the mock and simplifying requireValue (its throw branch isn't exercised by any test here) fixes both the test-quality gap and the pipeline failure.

As per path instructions for **/*.test.{ts,js,mts,mjs,cts,cjs}: "Flag copied production algorithms, broad mocks that bypass the behavior under test, and conditionals that make a test pass without exercising its claim."

🧪 Proposed fix
 import { describe, expect, it, vi } from "vitest";

 import { setupHermesProviderInference } from "./hermes";

-vi.mock("../../private-networks", () => ({
-  isPrivateHostname: (hostname: string) => {
-    const privateHosts = new Set(["localhost", "host.docker.internal"]);
-    const privatePatterns = [
-      /^127\./,
-      /^10\./,
-      /^192\.168\./,
-      /^172\.(1[6-9]|2\d|3[01])\./,
-      /^169\.254\./,
-    ];
-    if (privateHosts.has(hostname)) return true;
-    if (hostname.endsWith(".internal") || hostname.endsWith(".local")) return true;
-    return privatePatterns.some((re) => re.test(hostname));
-  },
-}));
-
 function makeDeps(overrides: Record<string, unknown> = {}) {
   return {
     ...
-    requireValue: vi.fn((v: unknown, msg: string) => {
-      if (!v) throw new Error(msg);
-      return v;
-    }),
+    requireValue: vi.fn((v: unknown) => v),

Please verify the real isPrivateHostname classifies all test hosts as expected before merging, and doesn't require test-environment setup that the mock was papering over:

#!/bin/bash
fd private-networks.ts src/lib
cat -n src/lib/private-networks.ts

Also applies to: 50-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/inference-providers/hermes.test.ts` around lines 8 - 22,
Remove the hand-rolled `isPrivateHostname` mock from `hermes.test.ts` and let
`setupHermesProviderInference` use the real classifier from
`src/lib/private-networks.ts`; keep the test focused on the provider behavior,
not a copied production algorithm. Update the test fixtures/assertions to match
the real `isPrivateHostname` behavior for loopback, link-local, RFC-1918,
`.internal`, and `.local` hosts, and simplify the `requireValue` fake by
removing the untested throw branch. Use the existing
`setupHermesProviderInference` and `requireValue` test helpers to locate the
changes.

Sources: Path instructions, Pipeline failures

Comment on lines +32 to +44
if (endpointUrl) {
let parsedEndpoint: URL;
try {
parsedEndpoint = new URL(endpointUrl);
} catch {
throw new Error(`Invalid inference endpoint URL: ${endpointUrl}`);
}
if (isPrivateHostname(parsedEndpoint.hostname)) {
throw new Error(
`Inference endpoint URL points to a private or internal address "${parsedEndpoint.hostname}". Use a public 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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Redact endpointUrl before including it in the thrown error.

Line 37 interpolates the raw, user-supplied endpointUrl into the thrown Error message. Unlike command output elsewhere in this file (e.g. Line 138, which passes untrusted strings through redact() before surfacing), this path emits the raw string directly. If a user's endpoint URL embeds credentials or an API key (e.g. http://user:secret@host or ?api_key=...) and later fails validation, that secret could be echoed into logs/console by whatever catches this error upstream.

deps is already in scope at this point (it's the function's second parameter), so this can be fixed without reordering the destructuring.

🔒 Proposed fix
     try {
       parsedEndpoint = new URL(endpointUrl);
     } catch {
-      throw new Error(`Invalid inference endpoint URL: ${endpointUrl}`);
+      throw new Error(`Invalid inference endpoint URL: ${deps.redact(endpointUrl)}`);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (endpointUrl) {
let parsedEndpoint: URL;
try {
parsedEndpoint = new URL(endpointUrl);
} catch {
throw new Error(`Invalid inference endpoint URL: ${endpointUrl}`);
}
if (isPrivateHostname(parsedEndpoint.hostname)) {
throw new Error(
`Inference endpoint URL points to a private or internal address "${parsedEndpoint.hostname}". Use a public endpoint.`,
);
}
}
if (endpointUrl) {
let parsedEndpoint: URL;
try {
parsedEndpoint = new URL(endpointUrl);
} catch {
throw new Error(`Invalid inference endpoint URL: ${deps.redact(endpointUrl)}`);
}
if (isPrivateHostname(parsedEndpoint.hostname)) {
throw new Error(
`Inference endpoint URL points to a private or internal address "${parsedEndpoint.hostname}". Use a public endpoint.`,
);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/inference-providers/hermes.ts` around lines 32 - 44, The
validation error in the endpoint parsing block currently echoes the raw
user-supplied endpoint string, which can leak secrets. Update the error path in
the `endpointUrl` handling inside `hermes.ts` to redact the value before
throwing, using the existing `deps`/`redact()` pattern already used elsewhere in
this module. Keep the hostname-based private-address error as-is, but ensure any
thrown message that includes `endpointUrl` no longer exposes credentials or
query secrets.

Comment thread src/lib/policy/index.ts
Comment on lines +1068 to +1081
const np = parsed.network_policies as PolicyObject;
for (const [policyKey, policyVal] of Object.entries(np)) {
if (!isPolicyObject(policyVal)) continue;
const endpoints = (policyVal as PolicyObject).endpoints;
if (!Array.isArray(endpoints)) continue;
for (const ep of endpoints) {
if (isPolicyObject(ep) && "allowed_ips" in ep) {
console.error(
` Preset '${presetName}' contains 'allowed_ips' in policy '${policyKey}', which is not permitted in user-supplied presets: ${filePath}`,
);
return null;
}
}
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether the merge/apply path (openshell policy-add or the sandbox merge logic)
# enforces the same array-of-objects shape for `endpoints`, or tolerates alternate shapes
# that could carry `allowed_ips` past this pre-check.
rg -nP -C5 '\bnetwork_policies\b' src/lib/policy/index.ts | head -100
rg -nP -C3 'policy-add|--from-file|--from-dir' src/lib/policy/index.ts

Repository: NVIDIA/NemoClaw

Length of output: 4796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the validator and the merge/apply path around the reported lines.
sed -n '1040,1105p' src/lib/policy/index.ts
printf '\n--- merge path ---\n'
sed -n '430,560p' src/lib/policy/index.ts
printf '\n--- custom file/dir path ---\n'
sed -n '730,820p' src/lib/policy/index.ts

printf '\n--- allowed_ips references ---\n'
rg -n -C4 '"allowed_ips"|allowed_ips' src/lib/policy/index.ts

Repository: NVIDIA/NemoClaw

Length of output: 10894


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- textBasedMerge ---'
sed -n '380,460p' src/lib/policy/index.ts

printf '\n%s\n' '--- parseCurrentPolicy / helpers ---'
sed -n '1,140p' src/lib/policy/index.ts

printf '\n%s\n' '--- preset endpoint extraction / any related validation ---'
rg -n -C4 'function getPresetEndpoints|allowed_ips|endpoints' src/lib/policy/index.ts

Repository: NVIDIA/NemoClaw

Length of output: 10403


Fail closed on malformed network_policies entries
policyVal/endpoints shapes that don’t match the expected object/array form are currently skipped, but the merge/apply path preserves them and writes them into the live policy unchanged. Reject malformed entries here instead of continueing, so allowed_ips can’t slip past this security check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/policy/index.ts` around lines 1068 - 1081, In the network_policies
validation loop in policy parsing, malformed policy entries are being skipped
via policyVal/endpoints shape checks instead of rejected, which lets unsafe data
bypass the allowed_ips guard. Update the validation in the
parsed.network_policies traversal to fail closed when a policyKey entry is not a
PolicyObject or when endpoints is not an array, alongside the existing
allowed_ips check, so invalid preset entries are rejected rather than preserved
for later merge/apply.

Codebase growth guardrail bans if statements in test files.
Replace requireValue mock body with a single expression.

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@prekshivyas

Copy link
Copy Markdown
Collaborator Author

Superseded by #6087 — rebased cleanly onto main with no unrelated commits in the diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants