Skip to content

security: strip credentials from migration snapshots and enforce blueprint digest#156

Closed
gn00295120 wants to merge 6 commits intoNVIDIA:mainfrom
gn00295120:security/sandbox-credential-exposure-and-blueprint-bypass
Closed

security: strip credentials from migration snapshots and enforce blueprint digest#156
gn00295120 wants to merge 6 commits intoNVIDIA:mainfrom
gn00295120:security/sandbox-credential-exposure-and-blueprint-bypass

Conversation

@gn00295120
Copy link
Copy Markdown
Contributor

@gn00295120 gn00295120 commented Mar 17, 2026

Summary

Fixes two critical vulnerabilities that together enable a one-message supply chain attack: a single Telegram message can steal all host credentials from the sandbox and use them to push backdoors to GitHub and npm.

Vulnerability 1 (CRITICAL): Migration copies all host credentials into sandbox

createSnapshotBundle() in migration-state.ts calls cpSync(hostState.stateDir, ...) which copies the entire ~/.openclaw directory — including auth-profiles.json with live API keys, GitHub PATs, and npm tokens — verbatim into the sandbox filesystem.

A compromised agent (e.g., via the Telegram bridge injection in #118) can read /sandbox/.openclaw/agents/main/agent/auth-profiles.json and exfiltrate all credentials via api.telegram.org (which is allowed in the baseline network policy).

Fix: Two sanitization layers:

  • prepareSandboxState()sanitizeCredentialsInBundle(): deletes auth-profiles.json from agents/ subtree, strips credential fields from openclaw.json
  • createSnapshotBundle()sanitizeExternalRootSnapshot(): strips auth-profiles.json from external root snapshots (agentDir, workspace, skills) before archiving

Vulnerability 2 (HIGH): Blueprint integrity verification silently bypassed

verifyBlueprintDigest() in verify.ts checks if (manifest.digest && ...) — but blueprint.yaml ships with digest: "" which is falsy in JavaScript. The entire integrity check is silently skipped.

Fix: Empty/missing digest is now a hard verification failure. Also fixed parseManifestHeader in resolve.ts to properly strip YAML quotes and inline comments from the digest field via cleanYamlValue().

Changes by file

File Change
migration-state.ts Add stripCredentials(), sanitizeCredentialsInBundle(), sanitizeExternalRootSnapshot(), removeAuthProfileFiles()
verify.ts Empty digest → hard failure instead of silent pass
resolve.ts Add cleanYamlValue() — strips YAML quotes and inline comments from manifest fields
test/security-credential-exposure.test.js 9 tests: credential exposure PoC, digest bypass verification, fix verification

Attack chain (now broken at two points)

Telegram message → shell injection → read auth-profiles.json
                                     ↑ BLOCKED: file deleted
                                     
Blueprint tampering → digest: "" → verification passes
                                   ↑ BLOCKED: hard failure

Related PRs

PR Relationship
#148 Fixes shell injection root cause (run()runArgv())
#119 Fixes Telegram bridge injection (different file, no conflict)
#118 Issue: Telegram bridge injection — not addressed by this PR
#123 Fixes dangerouslyDisableDeviceAuth (different file, no conflict)

Test plan

  • 60 tests pass (51 original + 9 new security tests)
  • Manual: openclaw nemoclaw migrate completes with sanitized bundle
  • Manual: Verify sandbox agent functions without baked-in credentials (uses OpenShell provider injection)

Note on breaking change

The shipped blueprint.yaml has digest: "". After this change, workflows using that blueprint will fail verification until the digest is populated at release time. This is intentional — running with an unverified blueprint should be an explicit operator decision, not a silent default.

Summary by CodeRabbit

  • Security

    • Migration snapshots and bundles now remove authentication files and redact credential values with a visible sentinel to prevent accidental exposure during state preparation and packaging.
  • Tests

    • Added end-to-end tests validating auth file removal, credential redaction, preservation of non-credential configuration, and workspace file integrity.
  • Chores

    • Commit rules updated to allow a new "security" commit type.

Copilot AI review requested due to automatic review settings March 17, 2026 08:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens sandbox migration and blueprint integrity checks to prevent credential exposure in the sandbox bundle and to ensure blueprint tampering cannot bypass verification via an empty digest.

Changes:

  • Sanitize migration sandbox bundle output by removing auth-profiles.json and stripping credential-like fields from openclaw.json.
  • Update blueprint digest verification to fail when the manifest digest is missing/empty.
  • Add a new security-focused test/PoC file intended to demonstrate the vulnerabilities and verify the fixes.

Reviewed changes

Copilot reviewed 3 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/security-credential-exposure.test.js Adds a security PoC/test suite for credential exposure + digest verification behavior.
nemoclaw/src/commands/migration-state.ts Adds credential sanitization helpers and applies them during sandbox bundle preparation.
nemoclaw/src/blueprint/verify.ts Treats empty/missing digest as a verification error instead of silently passing.
nemoclaw/dist/commands/migration-state.js Built output reflecting the new sanitization behavior.
nemoclaw/dist/commands/migration-state.js.map Source map update for the built migration-state output.
nemoclaw/dist/commands/migration-state.d.ts.map Type map update for the built migration-state output.
nemoclaw/dist/blueprint/verify.js Built output reflecting the new empty-digest verification failure.
nemoclaw/dist/blueprint/verify.js.map Source map update for the built verify output.
nemoclaw/dist/blueprint/verify.d.ts.map Type map update for the built verify output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch 3 times, most recently from edb87b5 to a5b3097 Compare March 17, 2026 09:25
@gn00295120 gn00295120 requested a review from Copilot March 18, 2026 13:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens NemoClaw’s migration and blueprint handling by preventing credential leakage into the sandbox and tightening blueprint integrity verification.

Changes:

  • Sanitize migration snapshots/bundles by removing auth-profiles.json and stripping credential-like fields from openclaw.json.
  • Treat missing/empty blueprint digests as a verification failure and add basic YAML value “cleaning” for manifest header parsing.
  • Add a security-focused test suite intended to cover credential exposure and digest enforcement scenarios.

Reviewed changes

Copilot reviewed 4 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/security-credential-exposure.test.js Adds security tests for credential exposure and blueprint digest enforcement (but currently reimplements logic instead of calling production code).
nemoclaw/src/commands/migration-state.ts Adds credential sanitization for prepared sandbox state and external root snapshots.
nemoclaw/src/blueprint/verify.ts Fails verification when manifest digest is missing/empty; improves error messaging.
nemoclaw/src/blueprint/resolve.ts Adds cleanYamlValue to strip quotes/comments from manifest header fields (currently has a parsing bug for quoted values with comments).
nemoclaw/dist/commands/migration-state.js Built output for migration sanitization changes.
nemoclaw/dist/commands/migration-state.js.map Sourcemap update for migration-state build output.
nemoclaw/dist/commands/migration-state.d.ts.map Type sourcemap update for migration-state build output.
nemoclaw/dist/blueprint/verify.js Built output for blueprint digest verification changes.
nemoclaw/dist/blueprint/verify.js.map Sourcemap update for blueprint verify build output.
nemoclaw/dist/blueprint/verify.d.ts.map Type sourcemap update for blueprint verify build output.
nemoclaw/dist/blueprint/resolve.js Built output for blueprint manifest parsing changes.
nemoclaw/dist/blueprint/resolve.js.map Sourcemap update for blueprint resolve build output.
nemoclaw/dist/blueprint/resolve.d.ts.map Type sourcemap update for blueprint resolve build output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 19, 2026
1. cleanYamlValue: remove inline comment BEFORE stripping surrounding
   quotes so that `""  # Computed at release time` correctly yields an
   empty string instead of the still-quoted `""`.

2. cleanYamlValue: broaden the inline-comment regex from `/\s+#\s/` to
   `/\s+#/` so that YAML comments without a trailing space (e.g.
   `abc123 #no-space`) are also stripped, matching the YAML spec.

3. walkAndRemoveFile: wrap `readdirSync(dirPath)` in its own try/catch
   and return early on failure so permission errors or transiently
   missing directories cannot abort snapshot creation (the function is
   documented as non-fatal).

4. Manifest-parsing tests now call the production `readCachedManifest`
   / `getCacheDir` exports instead of reimplementing the parsing logic,
   ensuring regressions in `cleanYamlValue` or `parseManifestHeader`
   are caught by the test suite.

5. "Fix verification" tests now call `detectHostOpenClaw` +
   `createSnapshotBundle` from the production migration-state module and
   assert on the produced bundle, replacing the previous duplicate local
   `stripCredentials` implementation.  A second negative test confirms
   that raw credential strings are absent from the bundle.

All 9 tests pass after the changes (node --test).
@wscurran wscurran added Migration Use this label to identify issues with migrating to NemoClaw from another framework. security Something isn't secure labels Mar 20, 2026
@wscurran
Copy link
Copy Markdown
Contributor

Thanks for addressing the critical vulnerabilities related to migration snapshots and blueprint digests, this could significantly improve the security of NemoClaw.

@wscurran wscurran added priority: high Important issue that should be resolved in the next release Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. labels Mar 20, 2026
@cv
Copy link
Copy Markdown
Contributor

cv commented Mar 21, 2026

Really appreciate the security work here, @gn00295120 — stripping credentials from migration snapshots and enforcing blueprint digest are both important hardening steps. Since this was opened, we've added CI checks and shipped quite a few changes, so the base has drifted a fair bit. Could you rebase this onto the latest main so we can evaluate it against the current state of things? Would love to get a fresh look at this. Thanks for the effort!

@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch from caa669d to 77fbc62 Compare March 21, 2026 23:10
@gn00295120
Copy link
Copy Markdown
Contributor Author

Thanks @cv — rebased onto latest main. Here's what changed during the rebase:

Scope reduced to credential sanitization only. The blueprint digest bypass fix (verify.ts / resolve.ts) was dropped because those modules were deleted upstream in #492 when the openclaw nemoclaw CLI commands were removed. The vulnerability no longer exists since there's no code path that calls the verification logic.

What remains (2 files changed):

  • migration-state.ts: sanitizeCredentialsInBundle() + sanitizeExternalRootSnapshot() — strips auth-profiles.json and credential fields from openclaw.json before the bundle enters the sandbox
  • test/security-credential-exposure.test.js: 2 tests (credential exposure PoC + fix verification), down from 9 — blueprint-related tests removed

The credential sanitization merged cleanly with the current codebase, including the pyright strict typing changes from #523. All tests pass.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 21, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds credential-sanitization utilities and applies them during migration bundle/snapshot creation: defines credential field allowlist, recursively strips credential values from openclaw.json, removes agents/*/auth-profiles.json, and sanitizes copied external-root snapshots before archiving.

Changes

Cohort / File(s) Summary
Credential sanitization logic
nemoclaw/src/commands/migration-state.ts
Adds CREDENTIAL_FIELDS allowlist and stripCredentials(); calls sanitizers to remove agents/*/auth-profiles.json, strip credential fields from written openclaw.json, and sanitize external-root snapshots after copying.
Security tests
test/security-credential-exposure.test.js
Adds tests reproducing pre-fix behavior and verifying post-fix: deletion of auth-profiles.json, credential fields replaced with "[STRIPPED_BY_MIGRATION]", non-credential fields and workspace files preserved, and temp cleanup.
Commit linting config
commitlint.config.js
Adds "security" to allowed commit type values in type-enum rule.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User as User
    participant MP as MigrationProcess
    participant SE as SanitizationEngine
    participant FS as FileSystem

    User->>MP: start snapshot/bundle creation
    MP->>FS: read sandbox (openclaw.json, agents/, external roots)
    MP->>SE: sanitize openclaw.json (CREDENTIAL_FIELDS)
    SE->>SE: recursively strip credential fields
    SE-->>MP: return sanitized openclaw.json
    MP->>FS: write sanitized openclaw.json
    MP->>FS: remove `agents/*/auth-profiles.json`
    MP->>FS: copy & sanitize external-root snapshots
    MP->>FS: archive sanitized bundle/snapshot
    FS-->>User: deliver sanitized snapshot/bundle
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I tiptoe through JSON, soft and bright,

I hide the tokens, tuck them out of sight.
Auth files hop away without a peep,
Openclaw wakes tidy, secrets put to sleep.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'security: strip credentials from migration snapshots and enforce blueprint digest' directly reflects the main changes in the PR: credential sanitization in migration-state.ts and test coverage. However, the blueprint digest enforcement mentioned in the title was removed during rebase because upstream modules were deleted, so the title partially overstates the current changeset scope.

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

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

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

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
nemoclaw/src/commands/migration-state.ts (2)

553-561: Consider extending credential field denylist for defense in depth.

The current list covers OpenClaw's known credential patterns, but additional common field names could provide broader protection:

  • accessToken / access_token
  • privateKey / private_key
  • secretKey / secret_key
  • refreshToken / refresh_token
  • bearer, authorization, credentials

Since auth-profiles.json is deleted entirely, this is a secondary defense layer. The current list likely covers existing OpenClaw schemas, but expanding it would guard against future credential fields.

🛡️ Suggested extension
 const CREDENTIAL_FIELDS = new Set([
   "apiKey",
   "api_key",
   "token",
   "secret",
   "password",
   "resolvedKey",
   "keyRef",
+  "accessToken",
+  "access_token",
+  "privateKey",
+  "private_key",
+  "secretKey",
+  "secret_key",
+  "refreshToken",
+  "refresh_token",
+  "bearer",
+  "authorization",
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 553 - 561, Update the
CREDENTIAL_FIELDS Set used in migration-state.ts to include additional common
credential field names for defense-in-depth: add accessToken and access_token,
privateKey and private_key, secretKey and secret_key, refreshToken and
refresh_token, bearer, authorization, credentials (and any camelCase/snake_case
variants you expect). Modify the CREDENTIAL_FIELDS constant so these additional
identifiers are present alongside the existing entries to ensure the denylist
catches more potential secret fields.

627-641: Consider logging failed file removals for observability.

The empty catch block silently swallows all errors. While individual failures may be non-fatal, completely suppressing them could hide situations where credential files weren't removed due to permission issues—undermining the security intent.

🔧 Suggested improvement
-function walkAndRemoveFile(dirPath: string, targetName: string): void {
+function walkAndRemoveFile(dirPath: string, targetName: string, logger?: PluginLogger): void {
   for (const entry of readdirSync(dirPath)) {
     const fullPath = path.join(dirPath, entry);
     try {
       const stat = lstatSync(fullPath);
       if (stat.isDirectory()) {
-        walkAndRemoveFile(fullPath, targetName);
+        walkAndRemoveFile(fullPath, targetName, logger);
       } else if (entry === targetName) {
         rmSync(fullPath, { force: true });
       }
-    } catch {
-      // Non-fatal: skip files that disappeared or lack permissions
+    } catch (err: unknown) {
+      // Non-fatal but worth logging for security audit
+      logger?.warn?.(`Failed to process ${fullPath}: ${err instanceof Error ? err.message : String(err)}`);
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 627 - 641, The empty
catch in walkAndRemoveFile silently swallows errors; change it to capture the
exception and log a warning/error including the fullPath and error details so
failed removals are observable (use the module's logger if available, e.g.,
logger.error/ warn, otherwise console.error). Keep behavior non-fatal (do not
rethrow), but ensure the log message names the function walkAndRemoveFile and
includes dirPath/targetName context and the thrown error to aid debugging and
auditing.
test/security-credential-exposure.test.js (1)

130-147: Test duplicates implementation logic instead of testing actual functions.

This test re-implements CREDENTIAL_FIELDS and stripCredentials rather than importing them from the source module. This means:

  1. If the implementation changes, this test won't catch regressions
  2. The test has subtly different logic—Line 140 only strips strings/objects, while the source (line 576 in migration-state.ts) strips all value types unconditionally

Consider importing and testing the actual exported functions, or at minimum, ensure the test logic matches the source exactly.

🔧 Align test logic with source or import actual functions

If keeping inline logic, match the source exactly:

         for (const [key, value] of Object.entries(obj)) {
-          if (CREDENTIAL_FIELDS.has(key) && (typeof value === "string" || typeof value === "object")) {
+          if (CREDENTIAL_FIELDS.has(key)) {
             result[key] = "[STRIPPED_BY_MIGRATION]";
           } else {
             result[key] = stripCredentials(value);
           }
         }

Alternatively, if stripCredentials and sanitizeCredentialsInBundle were exported, the test could import and exercise them directly for better regression coverage.

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

In `@test/security-credential-exposure.test.js` around lines 130 - 147, The test
re-implements CREDENTIAL_FIELDS and stripCredentials which can drift from the
source; update the test to either import the canonical definitions (e.g., import
CREDENTIAL_FIELDS, stripCredentials or sanitizeCredentialsInBundle from
migration-state.ts) and exercise those exports, or change the inline
stripCredentials to match the source behavior exactly by stripping credential
keys for all value types unconditionally (remove the current typeof check that
limits stripping to strings/objects) so test behavior matches the real
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 553-561: Update the CREDENTIAL_FIELDS Set used in
migration-state.ts to include additional common credential field names for
defense-in-depth: add accessToken and access_token, privateKey and private_key,
secretKey and secret_key, refreshToken and refresh_token, bearer, authorization,
credentials (and any camelCase/snake_case variants you expect). Modify the
CREDENTIAL_FIELDS constant so these additional identifiers are present alongside
the existing entries to ensure the denylist catches more potential secret
fields.
- Around line 627-641: The empty catch in walkAndRemoveFile silently swallows
errors; change it to capture the exception and log a warning/error including the
fullPath and error details so failed removals are observable (use the module's
logger if available, e.g., logger.error/ warn, otherwise console.error). Keep
behavior non-fatal (do not rethrow), but ensure the log message names the
function walkAndRemoveFile and includes dirPath/targetName context and the
thrown error to aid debugging and auditing.

In `@test/security-credential-exposure.test.js`:
- Around line 130-147: The test re-implements CREDENTIAL_FIELDS and
stripCredentials which can drift from the source; update the test to either
import the canonical definitions (e.g., import CREDENTIAL_FIELDS,
stripCredentials or sanitizeCredentialsInBundle from migration-state.ts) and
exercise those exports, or change the inline stripCredentials to match the
source behavior exactly by stripping credential keys for all value types
unconditionally (remove the current typeof check that limits stripping to
strings/objects) so test behavior matches the real implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5edec13d-48bd-4bac-bd86-ff284e7cd658

📥 Commits

Reviewing files that changed from the base of the PR and between 1dbf82f and 77fbc62.

📒 Files selected for processing (2)
  • nemoclaw/src/commands/migration-state.ts
  • test/security-credential-exposure.test.js

gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 21, 2026
C-2: Dockerfile CHAT_UI_URL Python code injection
  - Build-arg was interpolated directly into `python3 -c` source string
  - A single-quote in the URL closes the Python literal, allowing
    arbitrary code execution at image build time
  - Fix: promote to ENV, read via os.environ['CHAT_UI_URL']
  - Also fixes identical pattern for NEMOCLAW_MODEL
  - 11 tests: PoC proving injection, fix verification, regression guards

C-3: Telegram & Discord always-on in baseline sandbox policy
  - Both messaging APIs had no binaries: restriction in baseline
  - Any sandbox process could POST to attacker-controlled bots/webhooks
  - Fix: remove from baseline; opt-in presets already exist
  - 7 tests: host deny-list, binaries coverage invariant, preset existence

C-4: Snapshot manifest path traversal (arbitrary host write)
  - restoreSnapshotToHost() used manifest.stateDir/configPath as write
    targets with no path validation
  - Tampered snapshot.json could overwrite arbitrary host files
  - Fix: validate paths within manifest.homeDir via isWithinRoot()
  - 9 tests: PoC proving traversal, fix rejection, legitimate path success

Also updates audit report to mark C-1 as tracked by PR NVIDIA#156.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 21, 2026
10 parallel security scans across the entire NemoClaw codebase (~9,600 LoC)
identified 3 CRITICAL, 25 HIGH, 28 MEDIUM, and 16 LOW severity findings.

Critical findings (fixed in companion PRs):
- C-2: CHAT_UI_URL Python code injection in Dockerfile
- C-3: Telegram/Discord always-on in baseline policy (exfil channels)
- C-4: Snapshot manifest path traversal (arbitrary host write)
- C-1: Migration credential exposure (tracked by PR NVIDIA#156)

Also adds DRAFT-*.md to .gitignore to prevent accidental disclosure.
@gn00295120 gn00295120 requested a review from Copilot March 22, 2026 00:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the migration snapshot/bundle flow to prevent host credentials (API keys/tokens) from being copied into the sandbox, and adds regression tests demonstrating the prior exposure and the intended sanitization behavior.

Changes:

  • Add bundle sanitization in migration-state.ts to remove auth-profiles.json and strip credential-like fields from openclaw.json.
  • Sanitize external root snapshots by removing auth-profiles.json before archiving/sending into the sandbox.
  • Add a security-focused test suite covering credential exposure and sanitization.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
nemoclaw/src/commands/migration-state.ts Adds credential stripping/removal during bundle preparation and external root snapshotting.
test/security-credential-exposure.test.js Adds tests demonstrating the credential exposure PoC and verifying sanitization outcomes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
nemoclaw/src/commands/migration-state.ts (1)

553-561: Consider expanding credential field coverage.

The allowlist covers common cases, but may miss other credential-bearing fields. Consider adding:

  • accessToken, access_token, refreshToken, refresh_token
  • privateKey, private_key
  • clientSecret, client_secret
  • bearer, bearerToken
  • credentials, auth

Alternatively, consider a pattern-based approach (e.g., matching fields ending in Key, Token, Secret) as a defense-in-depth measure, though this risks false positives.

💡 Proposed expansion
 const CREDENTIAL_FIELDS = new Set([
   "apiKey",
   "api_key",
   "token",
   "secret",
   "password",
   "resolvedKey",
   "keyRef",
+  "accessToken",
+  "access_token",
+  "refreshToken",
+  "refresh_token",
+  "privateKey",
+  "private_key",
+  "clientSecret",
+  "client_secret",
+  "bearerToken",
+  "bearer_token",
+  "credentials",
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.ts` around lines 553 - 561, The
CREDENTIAL_FIELDS Set in migration-state.ts is too narrow and may miss other
sensitive keys; update the CREDENTIAL_FIELDS constant to include additional
common names such as accessToken, access_token, refreshToken, refresh_token,
privateKey, private_key, clientSecret, client_secret, bearer, bearerToken,
credentials, and auth, and optionally add a defensive pattern-based check (e.g.,
regexp tests in the same module for field names ending with
Key|Token|Secret|SecretKey) to catch variants—modify references that use
CREDENTIAL_FIELDS (look for usages of the CREDENTIAL_FIELDS Set) to consult the
expanded allowlist or the new pattern matcher so credential-bearing fields are
consistently detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 612-615: sanitizeExternalRootSnapshot currently only deletes
auth-profiles.json; update it to also locate config files in the external root
(e.g., openclaw.json and any other bundle config JSONs) and apply the existing
stripCredentials(...) routine to those files before writing them back, while
keeping the walkAndRemoveFile(rootSnapshotDir, "auth-profiles.json") behavior;
locate sanitizeExternalRootSnapshot and call stripCredentials on each discovered
config file (read -> stripCredentials -> write) rather than only removing
auth-profiles.json so embedded credential fields are sanitized.

---

Nitpick comments:
In `@nemoclaw/src/commands/migration-state.ts`:
- Around line 553-561: The CREDENTIAL_FIELDS Set in migration-state.ts is too
narrow and may miss other sensitive keys; update the CREDENTIAL_FIELDS constant
to include additional common names such as accessToken, access_token,
refreshToken, refresh_token, privateKey, private_key, clientSecret,
client_secret, bearer, bearerToken, credentials, and auth, and optionally add a
defensive pattern-based check (e.g., regexp tests in the same module for field
names ending with Key|Token|Secret|SecretKey) to catch variants—modify
references that use CREDENTIAL_FIELDS (look for usages of the CREDENTIAL_FIELDS
Set) to consult the expanded allowlist or the new pattern matcher so
credential-bearing fields are consistently detected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dac7d3f9-cb1f-4599-ae10-aafcd9779ad7

📥 Commits

Reviewing files that changed from the base of the PR and between 2ea575d and 95af20d.

📒 Files selected for processing (1)
  • nemoclaw/src/commands/migration-state.ts

Fixes a critical vulnerability where createSnapshotBundle() copies the
entire ~/.openclaw directory — including auth-profiles.json with live
API keys, GitHub PATs, and npm tokens — into the sandbox filesystem.

A compromised agent can read these credentials and exfiltrate them.

Fix: Two sanitization layers in migration-state.ts:
- sanitizeCredentialsInBundle(): deletes auth-profiles.json from agents/
  subtree, strips credential fields from openclaw.json
- sanitizeExternalRootSnapshot(): strips auth-profiles.json from
  external root snapshots before archiving

Note: Blueprint digest bypass fix (verify.ts/resolve.ts) was dropped
from this PR — those modules were deleted upstream in PR NVIDIA#492 as dead
code when the openclaw nemoclaw CLI commands were removed.


Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
- Move readdirSync inside try/catch to handle permission errors
- Add { force: true } to rmSync for race-condition safety
- Log warnings instead of silently swallowing removal failures
- Remove unused node:crypto import from test


Signed-off-by: Lucas Wang <lucas_wang@lucas-futures.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@gn00295120 gn00295120 force-pushed the security/sandbox-credential-exposure-and-blueprint-bypass branch from 03c80da to 612a857 Compare March 22, 2026 01:19
…symlink guards

- Remove keyRef from CREDENTIAL_FIELDS (metadata, not a secret)
- Add CREDENTIAL_FIELD_PATTERN for broader credential detection
  (accessToken, refreshToken, privateKey, clientSecret, etc.)
- Use JSON5.parse instead of JSON.parse for openclaw.json consistency
- Add symlink guards in walkAndStripCredentials and walkAndRemoveFile
  to prevent modifying/deleting files outside the snapshot boundary
- Rewrite tests to align with production logic, add pattern matching,
  symlink safety and value-type coverage tests
Copy link
Copy Markdown
Contributor

@cv cv left a comment

Choose a reason for hiding this comment

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

Minor: three missing docstrings are causing the lint (docstring coverage) check to fail. Suggestions below should fix it.

}
for (const entry of entries) {
const fullPath = path.join(dirPath, entry);
try {
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.

Missing docstring — causing the docstring coverage lint failure.

Suggested change
try {
/** Recursively walk dirPath and remove any files matching targetName. */
function walkAndRemoveFile(dirPath: string, targetName: string): void {

const stateDir = path.join(tmpDir, ".openclaw");
fs.mkdirSync(stateDir, { recursive: true });

const config = {
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.

Missing docstring — contributes to the docstring coverage lint failure.

Suggested change
const config = {
/** Create a mock ~/.openclaw directory tree populated with fake credential files. */
function createMockOpenClawHome(tmpDir) {

}
}
return result;
}
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.

Missing docstring — contributes to the docstring coverage lint failure.

Suggested change
}
/** Local reimplementation of stripCredentials for test isolation. */
function stripCredentials(obj) {

Address PR review feedback from cv: add JSDoc comments to
walkAndRemoveFile, createMockOpenClawHome, and stripCredentials.
@wscurran wscurran requested a review from drobison00 March 23, 2026 16:43
ericksoa added a commit that referenced this pull request Mar 23, 2026
…print digest

1. Filter auth-profiles.json from snapshot bundles during createSnapshotBundle()
   using cpSync's filter option to exclude credential-sensitive basenames.

2. Strip gateway config (contains auth tokens) from sandbox openclaw.json
   in prepareSandboxState() — sandbox entrypoint regenerates it at startup.

3. Add blueprint digest verification: record SHA-256 of blueprint.yaml in
   SnapshotManifest at snapshot time, validate on restore. Empty/missing
   digest is a hard failure; old snapshots without the field skip validation
   for backward compatibility.

Bump SNAPSHOT_VERSION 2→3 for the new manifest field.

Closes #156
@ericksoa ericksoa self-assigned this Mar 23, 2026
@cv
Copy link
Copy Markdown
Contributor

cv commented Mar 24, 2026

This overlaps significantly with #743 (ericksoa) but takes a different approach:

Neither is a strict superset of the other. #743 misses credential fields that aren't auth-profiles.json or gateway.auth.token (e.g., provider API keys in config). #156 misses blueprint digest verification.

These should be reconciled — either merge the pattern-based stripping from #156 into #743, or coordinate so they don't conflict. Both modify migration-state.ts and its tests heavily.

gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 24, 2026
… blueprint digest

Reconciles NVIDIA#156 and NVIDIA#743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from NVIDIA#743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from NVIDIA#156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Attack chain (now broken at multiple points):

  Telegram message → shell injection → read auth-profiles.json
                                       ↑ BLOCKED: filtered at copy
  Telegram message → shell injection → read openclaw.json credentials
                                       ↑ BLOCKED: stripped by pattern
  Blueprint tampering → digest: "" → verification passes
                                     ↑ BLOCKED: hard failure

Supersedes NVIDIA#156 and NVIDIA#743.
@gn00295120
Copy link
Copy Markdown
Contributor Author

Thanks for the analysis — agreed that neither is a strict superset.

I've opened #769 which reconciles both approaches into a single PR:

53 unit tests (41 existing + 12 new), 319/319 full suite pass.

#769

jyaunches added a commit to jyaunches/NemoClaw that referenced this pull request Mar 25, 2026
…nitization

Adds two new Brev E2E test suites targeting the vulnerabilities fixed by
PR NVIDIA#119 (Telegram bridge command injection) and PR NVIDIA#156 (credential
exposure in migration snapshots + blueprint digest bypass).

Test suites:
- test-telegram-injection.sh: 8 tests covering command substitution,
  backtick injection, quote-breakout, parameter expansion, process
  table leaks, and SANDBOX_NAME validation
- test-credential-sanitization.sh: 13 tests covering auth-profiles.json
  deletion, credential field stripping, non-credential preservation,
  symlink safety, blueprint digest verification, and pattern-based
  field detection

These tests are expected to FAIL on main (unfixed code) and PASS
once PR NVIDIA#119 and NVIDIA#156 are merged.

Refs: NVIDIA#118, NVIDIA#119, NVIDIA#156, NVIDIA#813
jyaunches added a commit to jyaunches/NemoClaw that referenced this pull request Mar 25, 2026
Adds specification for a warm-pool model that eliminates the 40+ min
cold-start Docker build time from Brev E2E test runs.

Key design:
- Pre-warmed Brev instances using NemoClaw launchable
- Naming convention (e2e-warm-*) as state — no external tracking
- PR comment trigger (/test-brev <suites>) for maintainers
- Parallel test execution across pool instances
- Daily health check + 24h instance cycling
- Fallback to ephemeral mode if pool is empty

6 phases: pool script → warmer workflow → test runner →
PR trigger → run security tests → cleanup

Also includes the original NVIDIA#813 hardware E2E spec for reference.

Refs: NVIDIA#813, NVIDIA#118, NVIDIA#119, NVIDIA#156
ericksoa added a commit that referenced this pull request Mar 25, 2026
… blueprint digest

- Filter auth-profiles.json from snapshot bundles during createSnapshotBundle()
- Strip gateway config (contains auth tokens) from sandbox openclaw.json
- Add blueprint digest verification: SHA-256 recorded at snapshot time, validated on restore
- Bump SNAPSHOT_VERSION 2 → 3

Closes #156
ericksoa added a commit that referenced this pull request Mar 25, 2026
… blueprint digest

- Filter auth-profiles.json from snapshot bundles during createSnapshotBundle()
- Strip gateway config (contains auth tokens) from sandbox openclaw.json
- Add blueprint digest verification: SHA-256 recorded at snapshot time, validated on restore
- Bump SNAPSHOT_VERSION 2 → 3

Closes #156
@cv cv closed this in 34296b8 Mar 25, 2026
gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 26, 2026
… blueprint digest

Reconciles NVIDIA#156 and NVIDIA#743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from NVIDIA#743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from NVIDIA#156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- computeFileDigest() throws when blueprint file is missing instead of
  silently returning null
- Sanitize both snapshot-level and sandbox-bundle openclaw.json copies
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Supersedes NVIDIA#156 and NVIDIA#743.
gn00295120 added a commit to gn00295120/NemoClaw that referenced this pull request Mar 26, 2026
… blueprint digest

Reconciles NVIDIA#156 and NVIDIA#743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from NVIDIA#743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from NVIDIA#156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- computeFileDigest() throws when blueprint file is missing instead of
  silently returning null
- Sanitize both snapshot-level and sandbox-bundle openclaw.json copies
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Supersedes NVIDIA#156 and NVIDIA#743.
@gn00295120 gn00295120 deleted the security/sandbox-credential-exposure-and-blueprint-bypass branch March 27, 2026 18:00
cv pushed a commit that referenced this pull request Mar 27, 2026
… blueprint digest (#769)

Reconciles #156 and #743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from #743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from #156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- computeFileDigest() throws when blueprint file is missing instead of
  silently returning null
- Sanitize both snapshot-level and sandbox-bundle openclaw.json copies
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Supersedes #156 and #743.
mafueee pushed a commit to mafueee/NemoClaw that referenced this pull request Mar 28, 2026
TSavo pushed a commit to wopr-network/nemoclaw that referenced this pull request Mar 28, 2026
* fix: improve gateway lifecycle recovery (NVIDIA#953)

* fix: improve gateway lifecycle recovery

* docs: fix readme markdown list spacing

* fix: tighten gateway lifecycle review follow-ups

* fix: simplify tokenized control ui output

* fix: restore chat route in control ui urls

* refactor: simplify ansi stripping in onboard

* fix: shorten control ui url output

* fix: move control ui below cli next steps

* fix: swap hard/soft ulimit settings in start script (NVIDIA#951)

Fixes NVIDIA#949

Co-authored-by: KJ <kejones@nvidia.com>

* chore: add cyclomatic complexity lint rule (NVIDIA#875)

* chore: add cyclomatic complexity rule (ratchet from 95)

Add ESLint complexity rule to bin/ and scripts/ to prevent new
functions from accumulating excessive branching. Starting threshold
is 95 (current worst offender: setupNim in onboard.js). Ratchet
plan: 95 → 40 → 25 → 15.

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

* chore: ratchet complexity to 20, suppress existing violations

Suppress 6 functions that exceed the threshold with eslint-disable
comments so we can start enforcing at 20 instead of 95:

- setupNim (95), setupPolicies (41), setupInference (22) in onboard.js
- deploy (22), main IIFE (27) in nemoclaw.js
- applyPreset (24) in policies.js

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

* chore: suppress complexity for 3 missed functions

preflight (23), getReconciledSandboxGatewayState (25), sandboxStatus (27)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add host-side config and state file locations to README (NVIDIA#903)

Signed-off-by: peteryuqin <peter.yuqin@gmail.com>

* chore: add tsconfig.cli.json, root execa, TS coverage ratchet (NVIDIA#913)

* chore: add tsconfig.cli.json, root execa, TS coverage ratchet

Foundation for the CLI TypeScript migration (PR 0 of the shell
consolidation plan). No runtime changes — config, tooling, and
dependency only.

- tsconfig.cli.json: strict TS type-checking for bin/ and scripts/
  (noEmit, module: preserve — tsx handles the runtime)
- scripts/check-coverage-ratchet.ts: pure TS replacement for the
  bash+python coverage ratchet script (same logic, same tolerance)
- execa ^9.6.1 added to root devDependencies (used by PR 1+)
- pr.yaml: coverage ratchet step now runs the TS version via tsx
- .pre-commit-config.yaml: SPDX headers cover scripts/*.ts,
  new tsc-check-cli pre-push hook
- CONTRIBUTING.md: document typecheck:cli task and CLI pre-push hook
- Delete scripts/check-coverage-ratchet.sh

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

* Apply suggestion from @brandonpelfrey

* chore: address PR feedback — use types_or, add tsx devDep

- Use `types_or: [ts, tsx]` instead of file glob for tsc-check-cli
  hook per @brandonpelfrey's suggestion.
- Add `tsx` to devDependencies so CI doesn't re-fetch it on every run
  per CodeRabbit's suggestion.

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

* fix(ci): ignore GitHub "Apply suggestion" commits in commitlint

* fix(ci): lint only PR title since repo is squash-merge only

Reverts the commitlint ignores rule from the previous commit and
instead removes the per-commit lint step entirely.

Individual commit messages are discarded at merge time — only the
squash-merged PR title lands in main and drives changelog generation.
Drop the per-commit lint, keep the PR title check, and remove the
now-unnecessary fetch-depth: 0.

* Revert "fix(ci): lint only PR title since repo is squash-merge only"

This reverts commit 1257a47.

* Revert "fix(ci): ignore GitHub "Apply suggestion" commits in commitlint"

This reverts commit c395657.

* docs: fix markdownlint MD032 in README (blank line before list)

* refactor: make coverage ratchet script idiomatic TypeScript

- Wrap in main() with process.exitCode instead of scattered process.exit()
- Replace mutable flags with .map()/.some() over typed MetricResult[]
- Separate pure logic (checkMetrics) from formatting (formatReport)
- Throw with { cause } chaining instead of exit-in-helpers
- Derive CoverageThresholds from METRICS tuple (single source of truth)
- Exhaustive switch on CheckStatus discriminated union

* refactor: remove duplication in coverage ratchet script

- Drop STATUS_LABELS map; inline labels in exhaustive switch
- Extract common 'metric coverage is N%' preamble in formatResult
- Simplify ratchetedThresholds: use results directly (already in
  METRICS order) instead of re-scanning with .find() per metric
- Compute 'failed' once in main, pass into formatReport to avoid
  duplicate .some() scan

* refactor: simplify coverage ratchet with FP patterns

- Extract classify() as a named pure function (replaces nested ternary)
- loadJSON takes repo-relative paths, eliminating THRESHOLD_PATH and
  SUMMARY_PATH constants (DRY the join-with-REPO_ROOT pattern)
- Drop CoverageMetric/CoverageSummary interfaces (only pct is read);
  use structural type at the call site instead
- Inline ratchetedThresholds (one-liner, used once)
- formatReport derives fail/improved from results instead of taking
  a pre-computed boolean (let functions derive from data, don't
  thread derived state)
- sections.join("\n\n") replaces manual empty-string pushing
- Shorter type names (Thresholds, Status, Result) — no ambiguity
  in a single-purpose script

* refactor: strip coverage ratchet to failure-only output

prek hides output from commands that exit 0, so ok/improved
reporting was dead code. Remove Status, Result, classify,
formatResult, formatReport, and the ratcheted-thresholds
suggestion block. The script now just filters for regressions
and prints actionable errors on failure.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Pelfrey <bpelfrey@nvidia.com>

* fix: use CONNECT tunnel for WebSocket endpoints in Discord/Slack presets (NVIDIA#438)

* fix: use CONNECT tunnel for WebSocket endpoints in Discord/Slack presets

The egress proxy's HTTP idle timeout (~2 min) kills long-lived WebSocket
connections when endpoints are configured with protocol:rest + tls:terminate.
Switch WebSocket endpoints to access:full (CONNECT tunnel) which bypasses
HTTP-level timeouts entirely.

Discord:
- gateway.discord.gg → access:full (WebSocket gateway)
- Add PUT/PATCH/DELETE methods for discord.com (message editing, reactions)
- Add media.discordapp.net for attachment access

Slack:
- Add wss-primary.slack.com and wss-backup.slack.com → access:full
  (Socket Mode WebSocket endpoints)

Partially addresses NVIDIA#409 — the policy-level fix enables WebSocket
connections to survive. The hardcoded 2-min timeout in openshell-sandbox
still affects any protocol:rest endpoints with long-lived connections.

Related: NVIDIA#361 (WhatsApp Web, same root cause)

* fix: correct comment wording for media endpoint and YAML formatting

* fix: standardize Node.js minimum version to 22.16 (NVIDIA#840)

* fix: remove unused RECOMMENDED_NODE_MAJOR from scripts/install.sh

Shellcheck flagged it as unused after the min/recommended merge.

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

* fix: enforce full semver >=22.16.0 in installer scripts

The runtime checks only compared the major Node.js version, allowing
22.0–22.15 to pass despite package.json requiring >=22.16.0. Use the
version_gte() helper for full semver comparison in both installers.

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

* fix: harden version_gte and align fallback message

Guard version_gte() against prerelease suffixes (e.g. "22.16.0-rc.1")
that would crash bash arithmetic. Also update the manual-install
fallback message to reference MIN_NODE_VERSION instead of hardcoded "22".

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

* fix: update test stubs for Node.js 22.16 minimum and add Node 20 rejection test

- Bump node stub in 'succeeds with acceptable Node.js' from v20.0.0 to v22.16.0
- Bump node stub in buildCurlPipeEnv from v22.14.0 to v22.16.0
- Add new test asserting Node.js 20 is rejected by ensure_supported_runtime

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: harden installer and onboard resiliency (NVIDIA#961)

* fix: harden installer and onboard resiliency

* fix: address installer and debug review follow-ups

* fix: harden onboard resume across later setup steps

* test: simplify payload extraction in onboard tests

* test: keep onboard payload extraction target-compatible

* chore: align onboard session lint with complexity rule

* fix: harden onboard session safety and lock handling

* fix: tighten onboard session redaction and metadata handling

* fix(security): strip credentials from migration snapshots and enforce blueprint digest (NVIDIA#769)

Reconciles NVIDIA#156 and NVIDIA#743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from NVIDIA#743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from NVIDIA#156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- computeFileDigest() throws when blueprint file is missing instead of
  silently returning null
- Sanitize both snapshot-level and sandbox-bundle openclaw.json copies
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Supersedes NVIDIA#156 and NVIDIA#743.

* fix(sandbox): export proxy env vars with full NO_PROXY and persist across reconnects (NVIDIA#1025)

* fix(sandbox): export proxy env vars with full NO_PROXY and persist across reconnects

OpenShell injects NO_PROXY=127.0.0.1,localhost,::1 into the sandbox, missing
inference.local and the gateway IP (10.200.0.1). This causes LLM inference
requests to route through the egress proxy instead of going direct, and the
proxy gateway IP itself gets proxied.

Add proxy configuration block to nemoclaw-start.sh that:
- Exports HTTP_PROXY, HTTPS_PROXY, and NO_PROXY with inference.local and
  the gateway IP included
- Persists via /etc/profile.d/nemoclaw-proxy.sh (root) or ~/.profile
  (non-root fallback) so values survive OpenShell reconnect injection
- Supports NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT overrides

The non-root fallback ensures the fix works in environments like Brev where
containers run without root privileges.

Tested on DGX Spark (ARM64) and Brev VM (x86_64). Verified NO_PROXY contains
inference.local and 10.200.0.1 inside the live sandbox after connect.

Ref: NVIDIA#626, NVIDIA#704
Ref: NVIDIA#704 (comment)

* fix(sandbox): write proxy config to ~/.bashrc for interactive reconnect sessions

OpenShell's `sandbox connect` spawns `/bin/bash -i` (interactive, non-login),
which sources ~/.bashrc — not ~/.profile or /etc/profile.d/*.  The previous
approach wrote to ~/.profile and /etc/profile.d/, neither of which is sourced
by `bash -i`, so the narrow OpenShell-injected NO_PROXY persisted in live
interactive sessions.

Changes:
- Write proxy snippet to ~/.bashrc (primary) and ~/.profile (login fallback)
- Export both uppercase and lowercase proxy variants (NO_PROXY + no_proxy,
  HTTP_PROXY + http_proxy, etc.) — Node.js undici prefers lowercase no_proxy
  over uppercase NO_PROXY when both are set
- Add idempotency guard to prevent duplicate blocks on container restart
- Update tests: verify .bashrc writing, idempotency, bash -i override
  behavior, and lowercase variant correctness

Tested on DGX Spark (ARM64) and Brev VM (x86_64) with full destroy +
re-onboard + live `env | grep proxy` verification inside the sandbox shell
via `openshell sandbox connect`.

Ref: NVIDIA#626

* fix(sandbox): replace stale proxy values on restart with begin/end markers

Use begin/end markers in .bashrc/.profile proxy snippet so
_write_proxy_snippet replaces the block when PROXY_HOST/PORT change
instead of silently keeping stale values. Adds test coverage for the
replacement path.

Addresses CodeRabbit review feedback on idempotency gap.

* fix(sandbox): resolve sandbox user home dynamically when running as root

When the entrypoint runs as root, $HOME is /root — the proxy snippet
was written to /root/.bashrc instead of the sandbox user's home.
Use getent passwd to look up the sandbox user's home when running as
UID 0; fall back to /sandbox if the user entry is missing.

Addresses CodeRabbit review feedback on _SANDBOX_HOME resolution.

---------

Co-authored-by: Carlos Villela <cvillela@nvidia.com>

* fix(policies): preset application for versionless policies (Fixes NVIDIA#35) (NVIDIA#101)

* fix(policies): allow preset application for versionless policies (Fixes NVIDIA#35)

Fixes NVIDIA#35

Signed-off-by: Deepak Jain <deepujain@gmail.com>

* fix: remove stale complexity suppression in policies

---------

Signed-off-by: Deepak Jain <deepujain@gmail.com>
Co-authored-by: Kevin Jones <kejones@nvidia.com>

* fix: restore routed inference and connect UX (NVIDIA#1037)

* fix: restore routed inference and connect UX

* fix: simplify detected local inference hint

* fix: remove stale local inference hint

* test: relax connect forward assertion

---------

Signed-off-by: peteryuqin <peter.yuqin@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Co-authored-by: KJ <kejones@nvidia.com>
Co-authored-by: Emily Wilkins <80470879+epwilkins@users.noreply.github.com>
Co-authored-by: Carlos Villela <cvillela@nvidia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter <peter.yuqin@gmail.com>
Co-authored-by: Brandon Pelfrey <bpelfrey@nvidia.com>
Co-authored-by: Benedikt Schackenberg <6381261+BenediktSchackenberg@users.noreply.github.com>
Co-authored-by: Lucas Wang <lucas_wang@lucas-futures.com>
Co-authored-by: senthilr-nv <senthilr@nvidia.com>
Co-authored-by: Deepak Jain <deepujain@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
jyaunches added a commit to jyaunches/NemoClaw that referenced this pull request Mar 30, 2026
…nitization

Adds two new Brev E2E test suites targeting the vulnerabilities fixed by
PR NVIDIA#119 (Telegram bridge command injection) and PR NVIDIA#156 (credential
exposure in migration snapshots + blueprint digest bypass).

Test suites:
- test-telegram-injection.sh: 8 tests covering command substitution,
  backtick injection, quote-breakout, parameter expansion, process
  table leaks, and SANDBOX_NAME validation
- test-credential-sanitization.sh: 13 tests covering auth-profiles.json
  deletion, credential field stripping, non-credential preservation,
  symlink safety, blueprint digest verification, and pattern-based
  field detection

These tests are expected to FAIL on main (unfixed code) and PASS
once PR NVIDIA#119 and NVIDIA#156 are merged.

Refs: NVIDIA#118, NVIDIA#119, NVIDIA#156, NVIDIA#813
cv added a commit that referenced this pull request Mar 30, 2026
…al sanitization (#1092)

* test(security): add E2E tests for command injection and credential sanitization

Adds two new Brev E2E test suites targeting the vulnerabilities fixed by
PR #119 (Telegram bridge command injection) and PR #156 (credential
exposure in migration snapshots + blueprint digest bypass).

Test suites:
- test-telegram-injection.sh: 8 tests covering command substitution,
  backtick injection, quote-breakout, parameter expansion, process
  table leaks, and SANDBOX_NAME validation
- test-credential-sanitization.sh: 13 tests covering auth-profiles.json
  deletion, credential field stripping, non-credential preservation,
  symlink safety, blueprint digest verification, and pattern-based
  field detection

These tests are expected to FAIL on main (unfixed code) and PASS
once PR #119 and #156 are merged.

Refs: #118, #119, #156, #813

* ci: temporarily disable repo guard for fork testing

* ci: bump bootstrap timeout, skip vLLM on CPU E2E runs

- Add SKIP_VLLM=1 support to brev-setup.sh
- Use SKIP_VLLM=1 in brev-e2e.test.js bootstrap
- Bump beforeAll timeout to 30 min for CPU instances
- Bump workflow timeout to 60 min for 3 test suites

* ci: bump bootstrap timeout to 40 min for sandbox image build

* ci: bump Brev instance to 8x32 for faster Docker builds

* ci: add real-time progress streaming for E2E bootstrap and tests

- Stream SSH output to CI log during bootstrap (no more silence)
- Add timestamps to brev-setup.sh and setup.sh info/warn/fail messages
- Add background progress reporter during sandbox Docker build (heartbeat every 30s showing elapsed time, current Docker step, and last log line)
- Stream test script output to CI log via tee + capture for assertions
- Filter potential secrets from progress heartbeat output

* ci: use NemoClaw launchable for E2E bootstrap

Replace bare 'brev create' + brev-setup.sh with 'brev start' using the
OpenShell-Community launch-nemoclaw.sh setup script. This installs Docker,
OpenShell CLI, and Node.js via the launchable's proven path, then runs
'nemoclaw onboard --non-interactive' to build the sandbox (testing whether
this path is faster than our manual setup.sh).

Changes:
- Default CPU back to 4x16 (8x32 didn't help — bottleneck was I/O)
- Launchable path: brev start + setup-script URL, poll for completion,
  rsync PR branch, npm ci, nemoclaw onboard
- Legacy path preserved (USE_LAUNCHABLE=0)
- Timestamped logging throughout for timing comparison
- New use_launchable workflow input (default: true)

* fix: prevent openshell sandbox create from hanging in non-interactive mode

openshell sandbox create without a command defaults to opening an interactive
shell inside the sandbox. In CI (non-interactive SSH), this hangs forever —
the sandbox goes Ready but the command never returns. The [?2004h] terminal
escape codes in CI logs were bash enabling bracketed paste mode, waiting for
input.

Add --no-tty -- true so the command exits immediately after the sandbox is
created and Ready.

* fix: source nvm in non-interactive SSH for launchable path

The launchable setup script installs Node.js via nvm, which sets up PATH
in ~/.nvm/nvm.sh. Non-interactive SSH doesn't source .bashrc, so npm/node
commands fail with 'command not found'. Source nvm.sh before running npm
in the launchable path and runRemoteTest.

* fix: setup.sh respects NEMOCLAW_SANDBOX_NAME env var

setup.sh defaulted to 'nemoclaw' ignoring the NEMOCLAW_SANDBOX_NAME env
var set by the CI test harness (e2e-test). Now uses $1 > $NEMOCLAW_SANDBOX_NAME > nemoclaw.

* ci: bump full E2E test timeout to 15 min for install + sandbox build

* ci: don't run full E2E alongside security tests (it destroys the sandbox)

The full E2E test runs install.sh --non-interactive which destroys and
rebuilds the sandbox. When TEST_SUITE=all, this kills the sandbox that
beforeAll created, causing credential-sanitization and telegram-injection
to fail with 'sandbox not running'. Only run full E2E when TEST_SUITE=full.

* ci: pre-build base image locally when GHCR image unavailable

On forks or before the first base-image workflow run, the GHCR base image
(ghcr.io/nvidia/nemoclaw/sandbox-base:latest) doesn't exist. This causes
the Dockerfile's FROM to fail. Now setup.sh checks for the base image
and builds Dockerfile.base locally if needed.

On subsequent builds, Docker layer cache makes this near-instant.
Once the GHCR base image is available, this becomes a no-op (docker pull
succeeds and the local build is skipped).

* ci: install nemoclaw CLI after bootstrap in non-launchable path

brev-setup.sh creates the sandbox but doesn't install the host-side
nemoclaw CLI that test scripts need for 'nemoclaw <name> status'.
Add npm install + build + link step after bootstrap.

* fix: use npm_config_prefix for nemoclaw CLI install so it lands on PATH

* fix: npm link from repo root where bin.nemoclaw is defined

* fix(ci): register sandbox in nemoclaw registry after setup.sh bootstrap

setup.sh creates the sandbox via openshell directly but never writes
~/.nemoclaw/sandboxes.json. The security test scripts check
`nemoclaw <name> status` which reads the registry, causing all E2E
runs to fail with 'Sandbox e2e-test not running'.

Write the registry entry after nemoclaw CLI install so the test
scripts can find the sandbox.

* style: shfmt formatting fix in setup.sh

* fix(test): exclude policy presets from C7 secret pattern scan

C7 greps for 'npm_' inside the sandbox and false-positives on
nemoclaw-blueprint/policies/presets/npm.yaml which contains rule
names like 'npm_yarn', not actual credentials. Filter out /policies/
paths from all three pattern checks.

* docs(ci): add test suite descriptions to e2e-brev workflow header

Document what each test_suite option runs so maintainers can make an
informed choice from the Actions UI without reading the test scripts.

* ci: re-enable repo guard for e2e-brev workflow

Re-enable the github.repository check so the workflow only runs on
NVIDIA/NemoClaw, not on forks.

* fix(test): update setup-sandbox-name test for NEMOCLAW_SANDBOX_NAME env var

setup.sh now uses ${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}} instead of
${1:-nemoclaw}. Update the test to match and add coverage for the env
var fallback path.

* fix(lint): add shellcheck directives for injection test payloads and fix stdio type

* fix(lint): suppress SC2034 for status_output in credential sanitization test

* fix: address CodeRabbit review — timeout, pipefail, fail-closed probes, shell injection in test

- Bump e2e-brev workflow timeout-minutes from 60 to 90
- Add fail-fast when launchable setup exceeds 40-min wait
- Add pipefail to remote pipeline commands in runRemoteTest and npm ci
- Fix backtick shell injection in validateName test loop (use process.argv)
- Make sandbox_exec fail closed with __PROBE_FAILED__ sentinel
- Add probe failure checks in C6/C7 sandbox assertions

---------

Co-authored-by: Carlos Villela <cvillela@nvidia.com>
ksapru pushed a commit to ksapru/NemoClaw that referenced this pull request Mar 30, 2026
… blueprint digest (NVIDIA#769)

Reconciles NVIDIA#156 and NVIDIA#743 into a single comprehensive solution:

- Filter auth-profiles.json at copy time via cpSync filter (from NVIDIA#743)
- Recursive stripCredentials() with pattern-based field detection for
  deep config sanitization (from NVIDIA#156: CREDENTIAL_FIELDS set +
  CREDENTIAL_FIELD_PATTERN regex)
- Remove gateway config section (contains auth tokens) from sandbox
  openclaw.json
- Blueprint digest verification (SHA-256): recorded at snapshot time,
  validated on restore, empty/missing digest is a hard failure
- computeFileDigest() throws when blueprint file is missing instead of
  silently returning null
- Sanitize both snapshot-level and sandbox-bundle openclaw.json copies
- Backward compatible: old snapshots without blueprintDigest skip
  validation
- Bump SNAPSHOT_VERSION 2 → 3

Supersedes NVIDIA#156 and NVIDIA#743.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. Migration Use this label to identify issues with migrating to NemoClaw from another framework. priority: high Important issue that should be resolved in the next release security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants