security: strip credentials from migration snapshots and enforce blueprint digest#156
Conversation
There was a problem hiding this comment.
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.jsonand stripping credential-like fields fromopenclaw.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.
edb87b5 to
a5b3097
Compare
There was a problem hiding this comment.
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.jsonand stripping credential-like fields fromopenclaw.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.
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).
|
Thanks for addressing the critical vulnerabilities related to migration snapshots and blueprint digests, this could significantly improve the security of NemoClaw. |
|
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! |
caa669d to
77fbc62
Compare
|
Thanks @cv — rebased onto latest main. Here's what changed during the rebase: Scope reduced to credential sanitization only. The blueprint digest bypass fix ( What remains (2 files changed):
The credential sanitization merged cleanly with the current codebase, including the pyright strict typing changes from #523. All tests pass. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds credential-sanitization utilities and applies them during migration bundle/snapshot creation: defines credential field allowlist, recursively strips credential values from Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment Tip CodeRabbit can generate a title for your PR based on the changes with custom instructions.Set the |
There was a problem hiding this comment.
🧹 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_tokenprivateKey/private_keysecretKey/secret_keyrefreshToken/refresh_tokenbearer,authorization,credentialsSince
auth-profiles.jsonis 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_FIELDSandstripCredentialsrather than importing them from the source module. This means:
- If the implementation changes, this test won't catch regressions
- 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
stripCredentialsandsanitizeCredentialsInBundlewere 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
📒 Files selected for processing (2)
nemoclaw/src/commands/migration-state.tstest/security-credential-exposure.test.js
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>
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.
There was a problem hiding this comment.
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.tsto removeauth-profiles.jsonand strip credential-like fields fromopenclaw.json. - Sanitize external root snapshots by removing
auth-profiles.jsonbefore 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.
There was a problem hiding this comment.
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_tokenprivateKey,private_keyclientSecret,client_secretbearer,bearerTokencredentials,authAlternatively, 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
📒 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>
There was a problem hiding this comment.
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.
03c80da to
612a857
Compare
…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
cv
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Missing docstring — causing the docstring coverage lint failure.
| 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 = { |
There was a problem hiding this comment.
Missing docstring — contributes to the docstring coverage lint failure.
| const config = { | |
| /** Create a mock ~/.openclaw directory tree populated with fake credential files. */ | |
| function createMockOpenClawHome(tmpDir) { |
| } | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Missing docstring — contributes to the docstring coverage lint failure.
| } | |
| /** Local reimplementation of stripCredentials for test isolation. */ | |
| function stripCredentials(obj) { |
Address PR review feedback from cv: add JSDoc comments to walkAndRemoveFile, createMockOpenClawHome, and stripCredentials.
…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
|
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 These should be reconciled — either merge the pattern-based stripping from #156 into #743, or coordinate so they don't conflict. Both modify |
… 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.
|
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 |
…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
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
… 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
… 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
… 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.
… 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.
… 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.
* 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>
…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
…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>
… 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.
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()inmigration-state.tscallscpSync(hostState.stateDir, ...)which copies the entire~/.openclawdirectory — includingauth-profiles.jsonwith 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.jsonand exfiltrate all credentials viaapi.telegram.org(which is allowed in the baseline network policy).Fix: Two sanitization layers:
prepareSandboxState()→sanitizeCredentialsInBundle(): deletesauth-profiles.jsonfromagents/subtree, strips credential fields fromopenclaw.jsoncreateSnapshotBundle()→sanitizeExternalRootSnapshot(): stripsauth-profiles.jsonfrom external root snapshots (agentDir, workspace, skills) before archivingVulnerability 2 (HIGH): Blueprint integrity verification silently bypassed
verifyBlueprintDigest()inverify.tschecksif (manifest.digest && ...)— butblueprint.yamlships withdigest: ""which is falsy in JavaScript. The entire integrity check is silently skipped.Fix: Empty/missing digest is now a hard verification failure. Also fixed
parseManifestHeaderinresolve.tsto properly strip YAML quotes and inline comments from the digest field viacleanYamlValue().Changes by file
migration-state.tsstripCredentials(),sanitizeCredentialsInBundle(),sanitizeExternalRootSnapshot(),removeAuthProfileFiles()verify.tsresolve.tscleanYamlValue()— strips YAML quotes and inline comments from manifest fieldstest/security-credential-exposure.test.jsAttack chain (now broken at two points)
Related PRs
run()→runArgv())dangerouslyDisableDeviceAuth(different file, no conflict)Test plan
openclaw nemoclaw migratecompletes with sanitized bundleNote on breaking change
The shipped
blueprint.yamlhasdigest: "". 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
Tests
Chores