fix(shields): preserve managed MCP policies - #8238
Conversation
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com> (cherry picked from commit 1803956) (cherry picked from commit 57e27a1a1b2df4bf16d52aeec078ebf6ec68d793)
📝 WalkthroughWalkthroughThe PR adds managed MCP policy inspection, ownership tracking, reconciliation, and deadline-aware restoration across Shields transitions. It updates runtime composition, timer recovery, audit reporting, tests, documentation, and MCP bridge E2E coverage. ChangesManaged MCP Shields lifecycle
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Timer
participant Shields as applyShieldsPolicySnapshot
participant Inspector as inspectProvableManagedMcpPoliciesForDeadline
participant Gateway
participant Audit
Timer->>Shields: restore snapshot with deadline authority
Shields->>Inspector: inspect current managed MCP policies
Inspector->>Gateway: read live policy and registry state
Gateway-->>Inspector: return policy data or inspection failures
Inspector-->>Shields: return exact policies and omissions
Shields-->>Timer: return applied policy result
Timer->>Audit: record omission warning
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
🌿 Preview your docs: https://nvidia-preview-pr-8238.docs.buildwithfern.com/nemoclaw |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
src/lib/shields/timer.test.ts (1)
16-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winType the mock return with the production
ManagedMcpPolicyOmissiontype.The local type requires
serveron every omission. The production type insrc/lib/actions/sandbox/mcp-bridge-policy.ts(lines 41-46) makesserver,key, andpolicyNameall optional and requires onlyreason. Real omissions frequently carry noserver, including the orphan registration case and the ownership-mismatch records thatapplyShieldsPolicySnapshotpushes.The narrower local type prevents a future fixture from reproducing those real shapes.
♻️ Proposed change to reuse the production type
+import type { ManagedMcpPolicyOmission } from "../actions/sandbox/mcp-bridge-policy"; + const shieldsIndexMock = vi.hoisted(() => ({ applyShieldsPolicySnapshot: vi.fn( (): { status: number; - managedMcpOmissions?: Array<{ server: string; reason: string }>; + managedMcpOmissions?: ManagedMcpPolicyOmission[]; } => ({ status: 0 }), ),
vi.hoistedruns before imports, so use a top-levelimport typeonly. A type-only import is erased and does not break hoisting.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/shields/timer.test.ts` around lines 16 - 21, Update the applyShieldsPolicySnapshot mock return type in the timer test to use the production ManagedMcpPolicyOmission type from mcp-bridge-policy.ts via a top-level type-only import, replacing the narrower local omission shape while preserving the mock behavior.src/lib/shields/mcp-policy-transition.ts (1)
12-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare one definition of the reserved MCP key namespace and the policy parse helpers.
RESERVED_MANAGED_MCP_POLICY_KEY_REhere andkey.startsWith("mcp_bridge_")insrc/lib/actions/sandbox/mcp-bridge-policy.ts(lines 246, 264, 302) define the same security-relevant namespace twice.parsePolicyDocumentandreadNetworkPoliciesalso duplicateparseManagedPolicyDocumentandreadManagedNetworkPoliciesin that file (lines 61-84).The strict inspector classifies reserved keys and this composer strips them. If one definition changes, the two sides disagree without a failing test. Extract the predicate and both parse helpers into one module that both files import.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/shields/mcp-policy-transition.ts` around lines 12 - 38, Extract the reserved MCP key predicate represented by RESERVED_MANAGED_MCP_POLICY_KEY_RE and the shared YAML helpers parsePolicyDocument/readNetworkPolicies into a common module. Update mcp-policy-transition.ts and the symbols parseManagedPolicyDocument, readManagedNetworkPolicies, and key.startsWith("mcp_bridge_") in mcp-bridge-policy.ts to import and reuse those shared definitions, removing the duplicate implementations while preserving current validation and stripping behavior.src/lib/shields/index.test.ts (2)
568-586: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCapture the applied policy through
buildPolicySetCommandinstead offs.rmSync.This test reads the staged policy by intercepting
fs.rmSyncand reading the temp directory before removal. That works only whilecleanupTempDiruses synchronousfs.rmSyncon the temp parent. A switch tofs.promises.rmwould leaveappliedPolicyempty, and the assertions at lines 587-588 would then pass or fail for the wrong reason.
src/lib/shields/flow.test.tsandsrc/lib/shields/policy-transition.test.tsboth capture the body inside abuildPolicySetCommandmock. That reads the file while it still exists and asserts the same property through the public boundary.The omission assertion at line 585 also matches
/Cannot read config file:/, an error string produced by the registry module. Prefer a match on the reconciliation-level wording that this module owns, so an unrelated message change in the registry does not fail this test.As per path instructions: "Prefer observable outcomes through the public boundary over source-text, private-shape, or mock-call assertions."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/shields/index.test.ts` around lines 568 - 586, Update the test setup around applyShieldsPolicySnapshot to capture the staged policy by mocking buildPolicySetCommand, reading its command body while the file exists, and removing the fs.rmSync interception and appliedPolicy logic. Keep the assertion through the public applyShieldsPolicySnapshot result, but match the reconciliation-level omission wording owned by this module rather than the registry-specific “Cannot read config file:” text.Source: Path instructions
110-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace the boolean
Mapand booleanswitchwith plain conditionals.
readFileWithUnreadableRegistrybuilds a two-entryMapkeyed bytrue/false, then immediately looks up one key and asserts non-null.readRuntimePolicyBeforeCleanupswitches over a boolean expression. Both are indirect forms of a singleif. They cost a reader more than they save.♻️ Proposed simplification
function readFileWithUnreadableRegistry( originalReadFileSync: typeof fs.readFileSync, file: fs.PathOrFileDescriptor, options?: unknown, ): unknown { - const readers = new Map<boolean, () => unknown>([ - [true, throwRegistryPermissionDenied], - [false, () => originalReadFileSync(file, options as never)], - ]); - return readers.get(String(file).endsWith(`${path.sep}sandboxes.json`))!(); + if (String(file).endsWith(`${path.sep}sandboxes.json`)) throwRegistryPermissionDenied(); + return originalReadFileSync(file, options as never); }function readRuntimePolicyBeforeCleanup( cleanupDir: string, readFile: typeof fs.readFileSync, ): string | null { - switch ( - path.basename(cleanupDir).startsWith("nemoclaw-permissive-runtime-") && - fs.existsSync(cleanupDir) - ) { - case false: - return null; - case true: { - const policyFile = fs.readdirSync(cleanupDir).find((name) => name.endsWith(".yaml")); - return policyFile ? readFile(path.join(cleanupDir, policyFile), "utf-8") : null; - } - } + if (!path.basename(cleanupDir).startsWith("nemoclaw-permissive-runtime-")) return null; + if (!fs.existsSync(cleanupDir)) return null; + const policyFile = fs.readdirSync(cleanupDir).find((name) => name.endsWith(".yaml")); + return policyFile ? readFile(path.join(cleanupDir, policyFile), "utf-8") : null; }Also applies to: 135-150
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/shields/index.test.ts` around lines 110 - 120, Replace the boolean-keyed Map in readFileWithUnreadableRegistry with a direct if conditional that calls throwRegistryPermissionDenied for sandboxes.json and originalReadFileSync otherwise. Also simplify the boolean switch in readRuntimePolicyBeforeCleanup to an equivalent plain conditional, preserving both existing branches and behavior.src/lib/shields/index.ts (1)
3070-3079: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConfirm the rollback restore still matches persisted state.
rollbackShieldsDownnow callsapplyShieldsPolicySnapshotwithout options. That call requiresstate.shieldsPolicySnapshotPath === snapshotPath, otherwise it throws "Shields state does not match the policy snapshot being restored". Both rollback call sites run aftersaveShieldsStatepersistsshieldsPolicySnapshotPath, so the check passes today. It becomes an ordering constraint that is not visible at this call site.Add a short comment that records the dependency, so a future move of the rollback call before
saveShieldsStatedoes not silently degrade every rollback to the warning branch at line 3096.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/shields/index.ts` around lines 3070 - 3079, Add a concise comment immediately before the applyShieldsPolicySnapshot call in rollbackShieldsDown documenting that the no-options invocation requires saveShieldsState to have persisted a matching shieldsPolicySnapshotPath first. Keep the rollback logic unchanged and make the ordering dependency explicit for both rollback call sites if applicable.test/permissive-runtime.test.ts (1)
218-235: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the remaining fail-closed cases for managed MCP policies.
This test covers one of three new fail-closed branches in
buildRuntimePermissivePolicy. Two branches remain uncovered:
- Line 121: the base policy parses to a non-object, and the function throws "Cannot parse the Shields-down policy while managed MCP policies are active".
- Lines 152-157 and 171-176: staging the temp policy fails, and the function throws "Cannot stage the Shields-down policy while managed MCP policies are active" instead of returning the static path.
The staging branch is the highest-value one. Without it, a regression that restores the old
return basePermissivePathfallback would silently drop every managed MCP entry, which is the exact#7952failure.💚 Proposed additional cases
it("fails closed when the base cannot be parsed with managed MCP policies active (`#7952`)", () => { expect(() => buildRuntimePermissivePolicy("/path/to/static.yaml", { livePolicyYaml: "version: 1\nnetwork_policies: {}\n", managedMcpPolicies: [MANAGED_POLICY], readBasePolicy: () => "::: not yaml :::", }), ).toThrow(/Cannot parse the Shields-down policy/); }); it("fails closed when staging fails with managed MCP policies active (`#7952`)", () => { expect(() => buildRuntimePermissivePolicy("/path/to/static.yaml", { livePolicyYaml: "version: 1\nnetwork_policies: {}\n", managedMcpPolicies: [MANAGED_POLICY], readBasePolicy: () => BASE_PERMISSIVE, writeTempPolicy: () => { throw new Error("ENOSPC: simulated /tmp full"); }, }), ).toThrow(/Cannot stage the Shields-down policy/); });As per path instructions for
src/lib/{security,credentials,shields}/**: "Require negative-path tests that prove the boundary rejects bypasses".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/permissive-runtime.test.ts` around lines 218 - 235, Add the two missing negative-path tests alongside the existing managed MCP fail-closed test: one using a non-object result from readBasePolicy to assert buildRuntimePermissivePolicy throws the parse error, and one using BASE_PERMISSIVE with writeTempPolicy throwing to assert it throws the staging error rather than returning the static path. Reuse the shared MANAGED_POLICY fixture and existing live policy setup.Source: Path instructions
src/lib/shields/permissive-runtime.ts (1)
195-198: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the unused
_basePolicyPathparameter, and wrap the deadline read error.Two small consistency items in the new public API:
buildRuntimeManagedMcpPolicyandbuildDeadlineRuntimeManagedMcpPolicynever use_basePolicyPath. Callers insrc/lib/shields/index.tsstill passsnapshotPathorbasePath, which suggests the argument affects the result.readBasePolicyis the only source of the base policy.buildRuntimeManagedMcpPolicywraps areadBasePolicyfailure with a descriptive message.buildDeadlineRuntimeManagedMcpPolicyat line 246 lets the raw error escape, so the deadline audit records a bareEACCESinstead of a policy-reconciliation reason.♻️ Proposed change for the deadline read error
- const baseYaml = deps.readBasePolicy(); + let baseYaml: string; + try { + baseYaml = deps.readBasePolicy(); + } catch (error) { + throw new Error("Cannot read the deadline Shields policy for managed MCP reconciliation", { + cause: error, + }); + }Removing the parameter requires updating the three call sites in
src/lib/shields/index.ts.Also applies to: 242-246
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/shields/permissive-runtime.ts` around lines 195 - 198, Remove the unused _basePolicyPath parameter from buildRuntimeManagedMcpPolicy and buildDeadlineRuntimeManagedMcpPolicy, then update all three callers in index.ts to stop passing snapshotPath or basePath. In buildDeadlineRuntimeManagedMcpPolicy, wrap readBasePolicy failures with the same descriptive policy-reconciliation context used by buildRuntimeManagedMcpPolicy before propagating the error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/manage-sandboxes/runtime-controls.mdx`:
- Around line 121-128: Move the entire MCP policy block from its current
location to the end of the section, after the existing “Verify each recorded
generation...” content, so the retry-attempt antecedent remains adjacent to its
deadline-gate sentence. In the moved block, change “snapshot-time-managed MCP
entries” to “snapshot-time managed MCP entries”; preserve all other wording and
behavior.
In `@src/lib/shields/flow.test.ts`:
- Line 231: Rename the test at line 231 to accurately reflect that it tests the
manual restoration path rather than the timer path. Since the test passes only
transitionProcessToken without deadlineAuthoritative, the
applyShieldsPolicySnapshot call takes the manual branch and calls
resolveExactManagedMcpPolicies, not the timer path. Update the test title to
describe manual restoration from persisted ownership to match the actual
implementation flow being tested.
---
Nitpick comments:
In `@src/lib/shields/index.test.ts`:
- Around line 568-586: Update the test setup around applyShieldsPolicySnapshot
to capture the staged policy by mocking buildPolicySetCommand, reading its
command body while the file exists, and removing the fs.rmSync interception and
appliedPolicy logic. Keep the assertion through the public
applyShieldsPolicySnapshot result, but match the reconciliation-level omission
wording owned by this module rather than the registry-specific “Cannot read
config file:” text.
- Around line 110-120: Replace the boolean-keyed Map in
readFileWithUnreadableRegistry with a direct if conditional that calls
throwRegistryPermissionDenied for sandboxes.json and originalReadFileSync
otherwise. Also simplify the boolean switch in readRuntimePolicyBeforeCleanup to
an equivalent plain conditional, preserving both existing branches and behavior.
In `@src/lib/shields/index.ts`:
- Around line 3070-3079: Add a concise comment immediately before the
applyShieldsPolicySnapshot call in rollbackShieldsDown documenting that the
no-options invocation requires saveShieldsState to have persisted a matching
shieldsPolicySnapshotPath first. Keep the rollback logic unchanged and make the
ordering dependency explicit for both rollback call sites if applicable.
In `@src/lib/shields/mcp-policy-transition.ts`:
- Around line 12-38: Extract the reserved MCP key predicate represented by
RESERVED_MANAGED_MCP_POLICY_KEY_RE and the shared YAML helpers
parsePolicyDocument/readNetworkPolicies into a common module. Update
mcp-policy-transition.ts and the symbols parseManagedPolicyDocument,
readManagedNetworkPolicies, and key.startsWith("mcp_bridge_") in
mcp-bridge-policy.ts to import and reuse those shared definitions, removing the
duplicate implementations while preserving current validation and stripping
behavior.
In `@src/lib/shields/permissive-runtime.ts`:
- Around line 195-198: Remove the unused _basePolicyPath parameter from
buildRuntimeManagedMcpPolicy and buildDeadlineRuntimeManagedMcpPolicy, then
update all three callers in index.ts to stop passing snapshotPath or basePath.
In buildDeadlineRuntimeManagedMcpPolicy, wrap readBasePolicy failures with the
same descriptive policy-reconciliation context used by
buildRuntimeManagedMcpPolicy before propagating the error.
In `@src/lib/shields/timer.test.ts`:
- Around line 16-21: Update the applyShieldsPolicySnapshot mock return type in
the timer test to use the production ManagedMcpPolicyOmission type from
mcp-bridge-policy.ts via a top-level type-only import, replacing the narrower
local omission shape while preserving the mock behavior.
In `@test/permissive-runtime.test.ts`:
- Around line 218-235: Add the two missing negative-path tests alongside the
existing managed MCP fail-closed test: one using a non-object result from
readBasePolicy to assert buildRuntimePermissivePolicy throws the parse error,
and one using BASE_PERMISSIVE with writeTempPolicy throwing to assert it throws
the staging error rather than returning the static path. Reuse the shared
MANAGED_POLICY fixture and existing live policy setup.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 196a14f3-5d40-4ea2-a478-a844dbae79e2
📒 Files selected for processing (20)
ci/source-architecture-budget.jsondocs/manage-sandboxes/runtime-controls.mdxdocs/reference/commands.mdxscripts/checks/openshell-policy-mutation-read.mtssrc/lib/actions/sandbox/mcp-bridge-policy.tssrc/lib/shields/flow.test.tssrc/lib/shields/index.test.tssrc/lib/shields/index.tssrc/lib/shields/mcp-policy-transition.test.tssrc/lib/shields/mcp-policy-transition.tssrc/lib/shields/permissive-runtime.tssrc/lib/shields/policy-transition.test.tssrc/lib/shields/timer.test.tssrc/lib/shields/timer.tstest/e2e/live/mcp-bridge-hermes-lifecycle.tstest/e2e/live/mcp-bridge-sandbox.tstest/e2e/live/mcp-bridge.test.tstest/e2e/support/mcp-bridge-sandbox.test.tstest/helpers/shields-flow-harness.tstest/permissive-runtime.test.ts
💤 Files with no reviewable changes (1)
- test/e2e/live/mcp-bridge-hermes-lifecycle.ts
PR Review Advisor — No blocking findings reportedAdvisor assessment: No blocking advisor findings reported Model lanes
7 terminology differences from the second opinionAdvisory only. These are normalized differences from the primary terminology receipt.
4 additional E2E selections from the second opinionAdvisory only. The primary lane did not select these E2E jobs or targets.
Second-opinion terminology and E2E selections are advisory. They do not change the primary assessment or E2E / PR Gate. Since last review: 0 prior items resolved · 0 still apply · 0 new items found 2 semantic terminology decisionsTerminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.
E2E guidanceAdvisory only. E2E / PR Gate selects and runs jobs independently. Recommended E2E: This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge. |
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
cv
left a comment
There was a problem hiding this comment.
The managed MCP reconciliation is otherwise well covered, but the deadline restore path introduces a blocking security-sensitive availability regression.
buildDeadlineRuntimeManagedMcpPolicy() always stages a new policy under os.tmpdir(), even when both the current managed policy set and snapshot ownership manifest are empty and the restrictive snapshot needs no modification. Because every new Shields-down transition persists an empty ownership array, sandboxes without MCP bridges now also require writable temp space to restore lockdown. If /tmp becomes full or unwritable during the Shields-down window, auto-restore cannot apply the already-safe snapshot; it exhausts its retries and can leave the relaxed policy active beyond the deadline.
Please reuse the original snapshot after validating that it has no reserved MCP keys and composition made no changes, and add an auto-restore regression for an empty-MCP restore with temp staging failing (for example, ENOSPC).
Local validation on this head otherwise passed: 176 focused tests, CLI typecheck, repository checks, Biome, diff check, and NUL-byte check.
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/reference/commands.mdx (1)
1031-1034: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDocument the buffer limit per stream.
maxBufferapplies separately to the pipedstdoutandstderrstreams, not to their combined size. The remaining behavior is accurate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/reference/commands.mdx` around lines 1031 - 1034, Update the non-JSON OpenClaw turns documentation to state that the 64 MiB capture limit applies independently to each piped stream, stdout and stderr, rather than to their combined output; preserve the surrounding replay, fallback-marker, and exit-status behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docs/reference/commands.mdx`:
- Around line 1031-1034: Update the non-JSON OpenClaw turns documentation to
state that the 64 MiB capture limit applies independently to each piped stream,
stdout and stderr, rather than to their combined output; preserve the
surrounding replay, fallback-marker, and exit-status behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: c5dcecca-ec1d-4262-8225-433f74de2fa0
📒 Files selected for processing (4)
ci/source-architecture-budget.jsondocs/reference/commands.mdxsrc/lib/shields/index.test.tssrc/lib/shields/permissive-runtime.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- ci/source-architecture-budget.json
- src/lib/shields/permissive-runtime.ts
Summary
Shields down previously replaced the complete live OpenShell policy, which removed NemoClaw-generated MCP entries and made a surviving Hermes MCP server unreachable. This change reconciles only exact, independently proven NemoClaw-managed MCP entries so server A stays reachable while a removed server B stays removed.
This is the focused MCP-policy change built on the lifecycle and deadline prerequisite merged in #8130. It replaces the focused behavior from the closed historical work in #7980 and #8141.
Related Issue
Fixes #7952
Changes
mcp restart Acoverage.Why this appeared during the Hermes upgrade
The original live journey contained a hidden lifecycle between the first successful call to A and the later B lifecycle:
A remained healthy through Shields up and the gateway restart. It became unusable immediately after Shields down, which dropped A's generated MCP policy. The later failure after B removal was only where the test detected the damage; B removal was a misleading correlation.
This surfaced alongside the Hermes upgrade because coverage and upgrade fixes landed close together:
mcp__fake__*tool naming, allowing the journey to progress far enough to expose the later failure.The whole-policy Shields replacement predates those changes. This is a latent NemoClaw Shields policy-composition bug exposed by expanded Hermes upgrade regression coverage, not evidence of a Hermes regression.
Corrected live regression order
Type of Change
Quality Gates
5d46d4a2ee924edf743ac36d04807948eec01c96passed all nine categories. The review covered managed-policy ownership, diagnostic sanitization, deadline restoration, empty-MCP staging failure, and current-main integration.Documentation Writer Review
docs-updated5d46d4a2ee924edf743ac36d04807948eec01c96against current main, including generated OpenClaw and Hermes variants, operator-facing assertions, test titles, and the empty-MCP deadline-restore regression. The current-main merge was mechanical, contributor attribution remains intact, and the existing documentation remains accurate. Normal hooks passed; GitHub CI is the current validation authority.DGX Station Hardware Evidence
scripts/prepare-dgx-station-host.shis unchanged.Verification
Signed-off-by:line and every commit appears asVerifiedin GitHubpre-commit,commit-msg, andpre-pushhooks passed, ornpm run validate:prpassed after refreshingorigin/mainwhen hooks were skipped or unavailable5d46d4a2ee924edf743ac36d04807948eec01c96.npm testfor broad runtime/test-harness changes;npm run checkfor repo-wide validation/coverage changes — command/result: GitHub CI is running for commit5d46d4a2ee924edf743ac36d04807948eec01c96.npm run docsbuilds without warnings (doc changes only) — GitHub CI is running for commit5d46d4a2ee924edf743ac36d04807948eec01c96.Signed-off-by: Julie Yaunches jyaunches@nvidia.com
Summary by CodeRabbit
New Features
Bug Fixes
Documentation