Skip to content

fix(shields): preserve managed MCP policies - #7980

Closed
jyaunches wants to merge 30 commits into
mainfrom
codex/fix-shields-mcp-policy
Closed

fix(shields): preserve managed MCP policies#7980
jyaunches wants to merge 30 commits into
mainfrom
codex/fix-shields-mcp-policy

Conversation

@jyaunches

@jyaunches jyaunches commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

nemoclaw shields down replaced the complete live OpenShell policy and dropped generated policy entries for registered Model Context Protocol (MCP) servers. This change reconciles only exact NemoClaw-managed MCP entries during Shields transitions, so a surviving server remains reachable while removed servers stay removed.

The Hermes live E2E now checks server A at the first surviving-server checkpoint after Shields down, before server B is added, and again after B is removed without restarting A first.

Related Issue

Fixes #7952

Changes

  • Prove managed MCP policy ownership from exact agreement between the sandbox registry, committed generated-policy record, and live gateway policy.
  • Save the owned MCP key manifest with the Shields snapshot, remove snapshot-time managed entries during restoration, and overlay only current exact entries.
  • Fail closed on ambiguous, stale, incomplete, malformed, or legacy ownership during manual transitions. At an expired deadline, omit unproven managed MCP entries and record the omission count instead of extending the shields-down window.
  • Bound interactive timer recovery to 7 attempts over one additional 30-second completion-grace window while the lifecycle deadline gate stays closed.
    If restoration cannot complete and commit, convert the deadline gate into durable containment before returning the failure.
    Recover only an exact, valid stale ordinary lifecycle-lock generation under the exclusive reaper gate.
    Keep durable containment for expired deadline owners, interrupted reapers, and unsafe or ambiguous generations where surviving descendants cannot be ruled out.
    Do not signal an active mutation process.
  • Clean staged runtime policy files on timer-start, state-persistence, policy-apply, and other early failure paths.
  • Preserve crash recovery for exact stale ordinary lifecycle locks while retaining fail-closed handling for corrupt, non-regular, wrong-sandbox, foreign, live, deadline, and reaper generations.
  • Put snapshot creation under the lifecycle gate before its timer-bound Shields transition.
  • Isolate non-live Vitest lifecycle and Shields state per test file so fixture lock failures cannot contaminate the caller's real NemoClaw state or unrelated tests.
  • Import the lifecycle-lock source directly in its behavioral suite so Vitest attributes coverage to the split acquisition and storage modules, while retaining a separate CommonJS instance only for the macOS probe spy.
  • Add co-located acquisition tests for synchronous nesting, deadline recovery, durable containment, and fail-closed containment reporting so those security paths retain deterministic coverage under CI sharding.
  • Pin the timer-marker symlink regression assertion to one O_NOFOLLOW file descriptor so its kind check and content read have no check-then-use window.
  • Add focused policy-composition, timer, lifecycle-lock, snapshot, and live E2E regression coverage.
  • Document MCP reconciliation, deadline-gate behavior, and operator recovery guidance.

Failure Timing and Hermes Upgrade Context

The earlier journey summary omitted an intermediate Shields lifecycle between the first successful call to A and the later lifecycle for B:

  1. Run shields up.
  2. Restart the Hermes gateway.
  3. Run shields down.
  4. Exercise the config rollback path.
  5. Add and remove B.
  6. Call A.

A remained healthy through Shields up and the gateway restart. Investigation placed the first failure at Shields down, which removed A's generated MCP policy. B removal was only where the prior test detected the already-broken route.

The failure became visible as live coverage changed:

This PR restores the meaningful order:

remove B -> call A -> restart A -> call A

The test checks A before any restart. The explicit restart without resupplying the secret predates #7866; #7866 added the authenticated rediscovery assertion. This PR keeps both after the survival assertion.

The whole-policy Shields replacement and filesystem-only runtime merge predate the Hermes upgrade. This is a latent NemoClaw Shields policy-composition defect detected by expanded Hermes regression coverage, not a Hermes upgrade regression.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification:
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: Independent Codex Desktop security review of commit 0f0987864b97f404b8be94844d2c178bdaeaf68d against base SHA 4cd4d64fe67143b57707f874afa0b9d269dfeff2 passed all nine categories with no findings: review comment.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Documentation Writer Review

  • Documentation writer subagent reviewed the completed changes
  • Result: docs-updated
  • Evidence: Independently reviewed the completed changes at exact head 0f0987864b97f404b8be94844d2c178bdaeaf68d. The existing source pages docs/manage-sandboxes/backup-restore.mdx, docs/manage-sandboxes/runtime-controls.mdx, and docs/reference/commands.mdx remain accurate and byte-identical. Restoring src/lib/actions/maintenance.ts and its tests to current main removes the redundant nested lock while retaining the stronger whole-backup lifecycle lock; ratcheting ci/source-architecture-budget.json is non-user-facing. No additional documentation edits are needed. Agent-variant sync remains represented by blob 3dd7c2425.
  • Agent: Codex Desktop

DGX Station Hardware Evidence

  • Tested on DGX Station
  • Tested commit:
  • Station profile/scenario:
  • Result:
  • Supporting evidence:

Verification

  • PR description includes a Signed-off-by: line and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run validate:pr passed after refreshing origin/main when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — the Shields matrix passed 5 files and 113 tests; the lifecycle/crash-recovery matrix passed 4 files and 71 tests; the test-linearization delta passed 3 files and 100 tests; the 257-policy boundary file passed all 29 tests after the finite timeout adjustment. The combined lifecycle-lock suite passed 42 tests, the co-located acquisition file passed 4 tests, and npm run test:changed passed its 4 selected tests. Merging the direct source map with the exact prior Linux eight-shard map puts acquisition at 83.78% functions and 67.48% branches and storage at 100% functions and 76.23% branches, above the 75%/60% security floors. The final CodeQL remediation passed 29 Shields flow tests and 33 combined focused tests. CLI typecheck, build, Biome, repository checks, test-project membership, source architecture, live-E2E structure, and the 1,945-file test-conditional scan passed.
  • Applicable broad gate passed — clean-runner CI for exact head f31b034f8 is pending.
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only) — build completed with 0 errors and 2 warnings.
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Julie Yaunches jyaunches@nvidia.com

@jyaunches jyaunches added area: docs Documentation, examples, guides, or docs build bug labels Jul 31, 2026
@jyaunches jyaunches self-assigned this Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

The change adds generation-aware lifecycle locks, managed MCP policy reconciliation, cooperative Shields auto-restore, sandbox mutation locking, isolated Vitest state, and expanded lifecycle and E2E coverage.

Changes

Shield lifecycle recovery

Layer / File(s) Summary
Lifecycle lock authority and containment
src/lib/state/mcp-lifecycle-lock*, src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts, src/lib/shields/transition-lock.ts
Lifecycle locks support synchronous acquisition, deadline fences, exact generation ownership, takeover-token validation, and permanent containment.
Managed MCP policy reconciliation
src/lib/actions/sandbox/mcp-bridge-policy.ts, src/lib/shields/mcp-policy-transition.ts, src/lib/shields/permissive-runtime.ts
Managed policies are validated, composed, retained, removed, or reported as omissions during policy restoration.
Shields transitions and auto-restore
src/lib/shields/index.ts, src/lib/shields/timer.ts, src/lib/shields/timer-control.ts
Shields transitions persist policy ownership and use deadline-authorized retries, cooperative completion, containment, and audit reporting.
Sandbox mutation and snapshot locking
src/lib/actions/maintenance.ts, src/lib/actions/sandbox/snapshot.ts, src/lib/actions/**/*test.ts
Backup-window and snapshot operations use serialized mutation locks and preserve lock ownership through asynchronous work.
Test isolation and E2E validation
test/helpers/isolate-test-state.ts, vitest.config.ts, test/vitest-temp-root.test.ts, test/e2e/live/*, test/e2e/support/*
Vitest projects receive private state directories. MCP bridge tests verify policy survival and lifecycle ordering.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: area: security, area: sandbox

Suggested reviewers: cv, cjagwani

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address all coding objectives in [#7952], including policy preservation, stale-snapshot safety, focused tests, and Hermes E2E coverage.
Out of Scope Changes check ✅ Passed The changes support [#7952] through lifecycle protection, snapshot locking, test isolation, cleanup, documentation, and required validation coverage.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: preserving managed MCP policies during Shields transitions.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-shields-mcp-policy

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

@github-code-quality

github-code-quality Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit 0f09878 in the codex/fix-shields-mc... branch remains at 96%, unchanged from commit 4cd4d64 in the main branch.

TypeScript / code-coverage/cli

The overall coverage in commit 0f09878 in the codex/fix-shields-mc... branch remains at 81%, unchanged from commit a931be4 in the main branch.

Show a code coverage summary of the most impacted files.
File main a931be4 codex/fix-shields-mc... 0f09878 +/-
src/lib/shields...sive-runtime.ts 96% 78% -18%
src/lib/shields...nsition-lock.ts 88% 85% -3%
src/lib/policy/index.ts 61% 59% -2%
src/lib/shields/index.ts 69% 68% -1%
src/lib/inferen...pter-forward.ts 92% 98% +6%
src/lib/state/m...-acquisition.ts 75% 83% +8%
src/lib/onboard...eway-service.ts 82% 90% +8%
src/lib/shields...imer-control.ts 80% 90% +10%
src/lib/shields...y-transition.ts 0% 86% +86%
src/lib/state/m...er-authority.ts 0% 100% +100%

Updated August 03, 2026 09:22 UTC

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — No blocking findings reported

Advisor assessment: No blocking advisor findings reported
Next action: No advisor follow-up needed.
Findings: 0 blockers · 0 warnings · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Completed · high confidence · 0 blockers · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Failed after a partial review · low confidence · 0 blockers · 0 warnings · 0 suggestions

Second-opinion terminology and E2E selections are advisory. They do not change the primary assessment or E2E / PR Gate.

4 semantic terminology decisions

Terminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.

  • define — lifecycle deadline gate at docs/manage-sandboxes/runtime-controls.mdx:111: Keep the term with its operational definition at first use on each affected documentation surface.
  • justified — durable containment at docs/manage-sandboxes/runtime-controls.mdx:125: Keep the modifier because it distinguishes persisted operator-resolved blocking from the temporary deadline gate.
  • justified — exact lock generation at docs/reference/commands.mdx:1165: Keep the modifier because the exact-generation requirement states the anti-reuse security constraint.
  • define — completion-grace window at docs/reference/commands.mdx:1167: Keep the term with the stated duration and restoration-only purpose.

E2E guidance

Advisory only. E2E / PR Gate selects and runs jobs independently.

Recommended E2E: cloud-inference, cloud-onboard, security-posture, inference-routing, mcp-bridge, mcp-bridge-dev, network-policy, onboard-repair, onboard-resume, rebuild-openclaw, state-backup-restore

Workflow run details

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/e2e/support/mcp-bridge-sandbox.test.ts (1)

305-334: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Replace source-text assertions with behavioral coverage.

Both tests inspect string positions in test/e2e/live/mcp-bridge.test.ts. They can pass when source text remains present but the E2E journey no longer performs the required public-boundary operation or validates its result.

  • test/e2e/support/mcp-bridge-sandbox.test.ts#L305-L334: Execute a focused scenario and assert the surviving policy outcome after route removal.
  • test/e2e/support/mcp-bridge-sandbox.test.ts#L336-L380: Execute a focused Hermes lifecycle scenario and assert tool-call outcomes before and after Shields, removal, restart, and rediscovery.

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 `@test/e2e/support/mcp-bridge-sandbox.test.ts` around lines 305 - 334, Replace
the source-text position and substring checks in
test/e2e/support/mcp-bridge-sandbox.test.ts:305-334 with a focused
public-boundary E2E scenario that performs route removal and asserts the
surviving managed-policy outcome. Replace the source-text assertions in
test/e2e/support/mcp-bridge-sandbox.test.ts:336-380 with a focused Hermes
lifecycle scenario that observes tool-call results before and after Shields,
removal, restart, and rediscovery. Use the existing public test helpers and
assert observable outcomes rather than source structure, private state, or mock
calls.

Source: Path instructions

🧹 Nitpick comments (16)
src/lib/shields/timer-bound-lock.ts (2)

84-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Accept injectable dependencies for symmetry.

withTimerBoundShieldsMutationLock and withTimerBoundShieldsMutationLockAsync both accept deps: TimerBoundLockDeps = defaultDeps. withTimerBoundAutoRestoreLock hardcodes defaultDeps, so a focused unit test cannot drive its token-generation retry loop without real state files. Add the same optional parameter.

♻️ Proposed change
 export function withTimerBoundAutoRestoreLock<T>(
   sandboxName: string,
   command: string,
   fn: () => T,
+  deps: TimerBoundLockDeps = defaultDeps,
 ): T {
   return withTimerBoundShieldsMutationLockOptions(
     sandboxName,
     command,
     fn,
     { recoverStaleOwner: false, waitTimeoutMs: 0 },
-    defaultDeps,
+    deps,
   );
 }
🤖 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-bound-lock.ts` around lines 84 - 96, Update
withTimerBoundAutoRestoreLock to accept an optional deps: TimerBoundLockDeps
parameter defaulting to defaultDeps, and pass that parameter to
withTimerBoundShieldsMutationLockOptions instead of hardcoding defaultDeps.
Preserve the existing call behavior for callers that omit deps.

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the lifecycle lock APIs from their owning module.

This block re-exports six src/lib/state/mcp-lifecycle-lock symbols from a shields helper. It creates a second import path for state-owned APIs, so a reader cannot tell from a call site which module owns lock ownership and containment. src/lib/shields/index.ts can import these names from ../state/mcp-lifecycle-lock directly.

Remove the forwarding block and update the shields callers to import from the state module.

Based on path instructions: "Review ownership against src/lib/README.md: actions orchestrate, domain modules make pure decisions, adapters own host/process/network boundaries, and state modules own persisted files and state I/O. Flag cross-layer cycles, duplicate sources of truth, and forwarding wrappers that add a new layer without retiring the old owner and its callers."

🤖 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-bound-lock.ts` around lines 11 - 18, Remove the
six-symbol forwarding export from the shields helper and update all shields
callers, including src/lib/shields/index.ts, to import these lifecycle lock APIs
directly from ../state/mcp-lifecycle-lock. Preserve the existing API usage while
ensuring the state module remains the sole ownership and import path.

Source: Path instructions

src/lib/state/mcp-lifecycle-lock-acquisition.ts (1)

366-379: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared containment and timeout diagnostics.

The containment message at Line 371 duplicates the message at Line 234 verbatim. The timeout messages already diverge: the async path adds "Another lifecycle, policy, channel, shields, or snapshot operation is still running." and uses owner pid, while this path uses owner PID and omits that sentence. Operators then see different guidance for the same condition depending on which entry point they hit.

Extract both strings into local helpers so the async and synchronous paths stay identical.

♻️ Proposed helpers
function containmentActiveMessage(
  sandboxName: string,
  lockPath: string,
  containmentPath: string,
  containment: LockObservation,
): string {
  return `Sandbox mutation containment is active for '${sandboxName}' at '${containmentPath}' (generation token '${containment.owner?.token ?? "invalid"}'). ...`;
}

function mutationLockTimeoutMessage(sandboxName: string, ownerPid: number | null): string {
  const ownerSuffix = ownerPid ? ` (owner pid ${ownerPid})` : "";
  return `Timed out waiting for the sandbox mutation lock for '${sandboxName}'${ownerSuffix}. Another lifecycle, policy, channel, shields, or snapshot operation is still running.`;
}
🤖 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/state/mcp-lifecycle-lock-acquisition.ts` around lines 366 - 379,
Extract shared local helpers for the containment-active and
mutation-lock-timeout diagnostics, and use them in both the asynchronous and
synchronous acquisition paths. Make the helpers produce identical wording,
including the containment details and the timeout’s lowercase “owner pid” suffix
plus the operation-in-progress guidance; update the visible synchronous branch
around readMcpLifecycleLockObservationSync and the corresponding async branch
without changing control flow.
src/lib/state/mcp-lifecycle-lock-storage.ts (1)

201-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the ordering invariants in the synchronous mirrors.

reclaimStaleMcpLifecycleLockGenerationSync and writeMcpLifecycleLockCandidateAndLinkSync copy the exact security-relevant sequence from their asynchronous counterparts but drop the comments that state why the sequence is required: rename is the atomic claim, the moved inode must be verified before deletion, a raced replacement is restored with a hard link that cannot overwrite a newer generation, and publication is decided only by LINK plus owner-token reconciliation. A maintainer who edits only the synchronous path can break mutual exclusion without seeing that reasoning.

Add a short comment in each synchronous function that points to the asynchronous function as the documented contract, and keep the two implementations in step.

♻️ Proposed comments
 export function reclaimStaleMcpLifecycleLockGenerationSync(
   targetPath: string,
   expected: LockObservation,
 ): boolean {
+  // Synchronous mirror of reclaimStaleMcpLifecycleLockGeneration. The rename is
+  // the atomic claim, the moved inode is verified before any deletion, and a
+  // raced replacement is restored with a hard link. Keep both in step.
   const quarantinePath = `${targetPath}.reclaim-${process.pid}-${crypto.randomUUID()}`;
 export function writeMcpLifecycleLockCandidateAndLinkSync(
   lockPath: string,
   owner: McpLifecycleLockOwner,
 ): boolean {
+  // Synchronous mirror of writeMcpLifecycleLockCandidateAndLink. The hard link
+  // is the atomic publication point, and EEXIST is only a failed claim after
+  // link-count plus owner-token reconciliation. Keep both in step.
   const candidatePath = `${lockPath}.candidate-${process.pid}-${owner.token}`;

Also applies to: 277-309

🤖 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/state/mcp-lifecycle-lock-storage.ts` around lines 201 - 234, Add
concise contract comments to reclaimStaleMcpLifecycleLockGenerationSync and
writeMcpLifecycleLockCandidateAndLinkSync, referencing their asynchronous
counterparts. Document the required ordering: atomic rename claim, verify the
moved inode before deletion, restore raced replacements via a non-overwriting
hard link, and decide publication only through LINK plus owner-token
reconciliation. Keep both synchronous implementations aligned with the
asynchronous contract.
src/lib/shields/permissive-runtime.ts (2)

246-251: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The deadline builder does not wrap read and compose failures.

buildRuntimeManagedMcpPolicy wraps a readBasePolicy failure as Cannot read the Shields policy for managed MCP reconciliation. buildDeadlineRuntimeManagedMcpPolicy calls deps.readBasePolicy() and composeDeadlineManagedMcpPolicies outside the try, so those failures propagate with the raw cause. Both paths still fail closed, so this is message consistency only. Move both calls inside the try if you want one diagnostic shape across the two builders.

🤖 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 246 - 251, Move the
readBasePolicy and composeDeadlineManagedMcpPolicies calls inside the try block
in buildDeadlineRuntimeManagedMcpPolicy, so both failures are wrapped with the
same diagnostic message used by buildRuntimeManagedMcpPolicy while preserving
fail-closed behavior.

195-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

basePolicyPath is unused in both new builders.

buildRuntimeManagedMcpPolicy and buildDeadlineRuntimeManagedMcpPolicy never read basePolicyPath. The base content arrives through deps.readBasePolicy(). The coding guidelines require a _ prefix for intentionally unused variables. Either prefix the parameter or remove it and update the two call sites in src/lib/shields/index.ts (lines 2646 and 2655) and the shields-down call site at line 3056.

Keeping an ignored path parameter also invites a future reader to assume the function reads that file.

As per coding guidelines: "Prefix intentionally unused variables with _ and keep function complexity low."

♻️ Minimal fix
 export function buildRuntimeManagedMcpPolicy(
-  basePolicyPath: string,
+  _basePolicyPath: string,
   deps: ManagedMcpRuntimePolicyDeps,
 ): string {
 export function buildDeadlineRuntimeManagedMcpPolicy(
-  basePolicyPath: string,
+  _basePolicyPath: string,
   deps: ManagedMcpRuntimePolicyDeps,
 ): DeadlineManagedMcpRuntimePolicy {

Also applies to: 242-245

🤖 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 their call sites in the
shields index flow, including the shields-down path, to match the new
signatures. Continue obtaining base policy content through
deps.readBasePolicy().

Source: Coding guidelines

src/lib/shields/timer.ts (1)

514-533: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the deadline-fence timing values.

pollIntervalMs: 50 and timeoutMs: 5_000 are inline literals in the lifecycle-critical restore path. This file already names its other timing budget as AUTO_RESTORE_RETRY_MS. Promote these two to named constants next to it so the fence acquisition budget is discoverable and tunable in one place.

🤖 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.ts` around lines 514 - 533, Define named constants for
the lifecycle fence poll interval and timeout alongside AUTO_RESTORE_RETRY_MS,
then replace the inline 50 and 5_000 values in the withMcpLifecycleDeadlineFence
options within the restore path with those constants.
src/lib/shields/transition-lock.ts (1)

493-511: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two inspect methods duplicate the owner projection.

inspectShieldsTransitionLockOwner and inspectAnyShieldsTransitionLockOwner repeat the same snapshot read, projection, and closeSnapshot sequence. Only the token predicate differs. Extract one private helper that takes an owner predicate so a future field added to InspectedShieldsTransitionOwner cannot diverge between the two paths.

♻️ Proposed shared helper
+  private inspectOwnerMatching(
+    sandboxName: string,
+    matches: (owner: ShieldsTransitionLockOwner) => boolean,
+  ): InspectedShieldsTransitionOwner | null {
+    const validName = validateSandboxName(sandboxName);
+    const lockPath = shieldsTransitionLockPath(validName, this.stateDir);
+    const snapshot = readExistingLock(lockPath, validName);
+    if (!snapshot) return null;
+    try {
+      const owner = snapshot.owner;
+      if (!owner || !matches(owner)) return null;
+      return {
+        pid: owner.pid,
+        processStartIdentity: owner.processStartIdentity,
+        command: owner.command,
+      };
+    } finally {
+      closeSnapshot(snapshot);
+    }
+  }

Also applies to: 1175-1180

🤖 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/transition-lock.ts` around lines 493 - 511, Extract the
shared snapshot-reading and owner-projection logic from
inspectShieldsTransitionLockOwner and inspectAnyShieldsTransitionLockOwner into
one private helper that accepts the differing owner/token predicate. Have both
public inspect methods delegate to this helper, preserving null handling and
closeSnapshot cleanup while centralizing construction of
InspectedShieldsTransitionOwner.
src/lib/shields/index.ts (1)

2662-2671: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The temp-file prefix is a repeated string literal.

cleanupTempDir(runtimePolicyPath, "nemoclaw-permissive-runtime") repeats the TEMP_FILE_PREFIX value defined in src/lib/shields/permissive-runtime.ts line 31. The same literal appears again at line 3191. cleanupTempDir only removes a directory whose basename starts with ${prefix}-, so a future rename of TEMP_FILE_PREFIX would silently stop cleaning up the staged policy directories instead of failing. Export the constant from permissive-runtime and import it here.

🤖 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 2662 - 2671, Export the existing
TEMP_FILE_PREFIX constant from permissive-runtime and import it into the code
containing the cleanupTempDir calls. Replace both repeated
"nemoclaw-permissive-runtime" literals, including the call near
buildPolicySetCommand and the one near line 3191, with the shared constant.
src/lib/shields/mcp-policy-transition.ts (1)

11-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The managed MCP policy grammar has two owners. The reserved key namespace and the policy-document parse helpers are defined independently in both modules. The inspector and the composer must agree on the same key namespace and the same document shape; two copies can drift so a key is classified in one path and unclassified in the other.

  • src/lib/shields/mcp-policy-transition.ts#L11-L37: keep RESERVED_MANAGED_MCP_POLICY_KEY_RE, CANONICAL_MANAGED_MCP_POLICY_KEY_RE, parsePolicyDocument, and readNetworkPolicies here as the single owner, and export the reserved-prefix predicate and the two document helpers.
  • src/lib/actions/sandbox/mcp-bridge-policy.ts#L243-L243: replace the four inline key.startsWith("mcp_bridge_") checks (lines 243, 298-300, 379-381, 457-459) with the exported predicate, and delete the local parseManagedPolicyDocument and readManagedNetworkPolicies duplicates at lines 60-83 in favor of the exported helpers.
🤖 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 11 - 37, Make
src/lib/shields/mcp-policy-transition.ts the single owner of MCP policy grammar
by exporting the reserved-prefix predicate derived from
RESERVED_MANAGED_MCP_POLICY_KEY_RE, along with parsePolicyDocument and
readNetworkPolicies. In src/lib/actions/sandbox/mcp-bridge-policy.ts at line 243
and the additional checks at lines 298-300, 379-381, and 457-459, replace inline
mcp_bridge_ checks with the exported predicate; remove
parseManagedPolicyDocument and readManagedNetworkPolicies at lines 60-83 and use
the exported helpers instead.
src/lib/shields/timer.test.ts (2)

11-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset completeAutoRestoreTransition in setup too.

Line 43 re-establishes the applyShieldsPolicySnapshot implementation for each test, but completeAutoRestoreTransition keeps only the implementation supplied at vi.hoisted time. This project enables restoreMocks, which clears implementations on vi.fn() mocks between tests. If that implementation is lost, the mock returns undefined, and src/lib/shields/timer.ts lines 441-449 treat a falsy return as revoked. The timer then skips the shields_auto_restore audit entry and skips cleanupOwnedTimerMarker, so the success test would fail for a reason unrelated to the behavior under test.

Reset both mocks in the same setup block so the tests do not depend on ordering.

As per coding guidelines: "In deterministic tests, clear mock calls, restore spies, undo environment/global stubs, and explicitly reset mock implementations when needed."

♻️ Proposed setup addition
     shieldsIndexMock.applyShieldsPolicySnapshot.mockImplementation(() => ({ status: 0 }));
+    shieldsIndexMock.completeAutoRestoreTransition.mockImplementation(() => true);

Also applies to: 29-33, 43-43

🤖 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 11 - 16, Update the test setup
around the existing applyShieldsPolicySnapshot reset to also restore
completeAutoRestoreTransition’s successful implementation for every test. Ensure
both mocks are explicitly reset in the same setup block so restoreMocks cannot
leave completeAutoRestoreTransition returning undefined.

Sources: Coding guidelines, Learnings


75-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The 200 ms file-existence poll can flake on a loaded runner.

invokeTimerAndExpectRetry polls at most 200 times with a 1 ms sleep, so the effective budget is roughly 200 ms plus scheduling overhead. The loop then falls through whether or not the files appeared, and line 107 asserts fs.existsSync(deadlinePath) is true. On a slow or contended CI runner the deadline file may not exist yet, and the test fails for timing reasons rather than behavior. Line 106 (expect(exitSpy).not.toHaveBeenCalled()) has the same dependency.

Raise the budget substantially and fail with an explicit message when the wait expires, so a timeout is distinguishable from a real regression.

♻️ Proposed polling change
-      for (let attempt = 0; attempt < 200; attempt += 1) {
+      const waitUntilMs = Date.now() + 10_000;
+      while (Date.now() < waitUntilMs) {
         if (
           (!deadlinePath || fs.existsSync(deadlinePath)) &&
           (!auditPath || fs.existsSync(auditPath))
         ) {
           break;
         }
-        await new Promise((resolve) => setTimeout(resolve, 1));
+        await new Promise((resolve) => setTimeout(resolve, 5));
       }
🤖 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 75 - 116, Update
invokeTimerAndExpectRetry to use a substantially longer polling timeout than the
current 200 attempts, and track whether the deadline and audit files become
ready. If the wait expires, fail explicitly with a descriptive timeout message
before the exit and deadline assertions; preserve the existing readiness
conditions and retry behavior once the files appear.
src/lib/shields/index.test.ts (2)

517-531: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the fs.readFileSync and process.kill spies in a finally block.

This test installs two broad spies: fs.readFileSync (which intercepts every read in the process) and process.kill. Neither is restored inside the test. If any assertion between lines 534 and 550 throws, both spies stay installed for the rest of this file. A leaked fs.readFileSync spy can then corrupt unrelated tests in ways that are hard to attribute.

Wrap the exercise and assertions in try/finally and call mockRestore() on both spies in the finally.

As per coding guidelines: "In deterministic tests, clear mock calls, restore spies, undo environment/global stubs, and explicitly reset mock implementations when needed." The retrieved learning states the same rule: restore locally created spies in try/finally so they are restored on failure paths, rather than relying only on the global restoreMocks behavior.

🤖 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 517 - 531, Wrap the test exercise
and assertions following the fs.readFileSync and process.kill spy setup in a
try/finally block. Store both spy handles, then call mockRestore() for each in
finally so they are restored even when assertions fail; keep the existing mock
behavior and assertions unchanged.

Sources: Coding guidelines, Learnings


486-494: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the applied policy instead of re-deriving the composition.

The test title claims that a deadline restore removes saved MCP keys. The only assertion about the restore itself is result.managedMcpOmissions at line 540. Lines 543-550 then call composeDeadlineManagedMcpPolicies again and assert on its output, which re-derives the expected result rather than checking what applyShieldsPolicySnapshot sent to the gateway. That duplicates the dedicated composition test at lines 486-494 and leaves the restore's observable outcome unverified.

Capture the policy file that applyShieldsPolicySnapshot passes to the policy-set command and assert that it retains restrictive_baseline and drops mcp_bridge_alpha.

Both blocks also assert on raw YAML substrings. Parsing the YAML and asserting on the network_policies keys is the stronger check, and src/lib/shields/mcp-policy-transition.test.ts line 606 already uses that form.

As per path instructions: "Prefer observable outcomes through the public boundary over source-text, private-shape, or mock-call assertions."

Also applies to: 543-551

🤖 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 486 - 494, Update the deadline
restore test around applyShieldsPolicySnapshot to capture the policy file
submitted to the gateway policy-set command and assert its observable applied
policy, rather than calling composeDeadlineManagedMcpPolicies again. Parse the
captured YAML and verify network_policies retains restrictive_baseline while
omitting mcp_bridge_alpha, replacing raw substring assertions and preserving the
existing managedMcpOmissions check.

Source: Path instructions

src/lib/shields/timer-control.ts (1)

219-255: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

terminated is now a constant false.

killTimer never signals the timer process, so KillTimerResult.terminated can only be false. The field is now dead information for callers. Consider removing it from the result contract, or documenting it as retained for compatibility with existing consumers.

Also, the nested condition at lines 228-232 can collapse into one check.

♻️ Collapse the redundant nesting
   if (marker) {
     wasAlive = isProcessAlive(marker.pid);
     if (wasAlive) {
       const verification = verifyTimerMarkerIdentity(marker);
-      if (!verification.verified) {
-        if (verification.warning) {
-          warnings.push(verification.warning);
-        }
-      }
+      if (!verification.verified && verification.warning) {
+        warnings.push(verification.warning);
+      }
     }
   }
🤖 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-control.ts` around lines 219 - 255, Update killTimer
and the KillTimerResult contract to address the constant terminated: false
field: remove it if callers can be migrated, or explicitly retain and document
it for compatibility. Also collapse the nested verification.warning condition in
killTimer into a single guarded check while preserving warning collection
behavior.
src/lib/shields/mcp-policy-transition.test.ts (1)

304-412: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative-path coverage for ambiguous current keys in the composers.

composeManagedMcpPolicies (lines 77-79 of src/lib/shields/mcp-policy-transition.ts) and composeDeadlineManagedMcpPolicies (lines 133-135) throw Managed MCP policy key '<key>' has ambiguous ownership for a duplicate or non-canonical policy.key in currentPolicies. No test exercises either throw. That guard is the last barrier before an unvalidated key is written into network_policies, so a regression would silently overlay an arbitrary key.

Add two cases: one with two currentPolicies entries sharing a key, and one with a key that fails CANONICAL_MANAGED_MCP_POLICY_KEY_RE (for example mcp_bridge_Alpha).

As per path instructions: "Require negative-path tests that prove the boundary rejects bypasses and does not leak secrets in errors, logs, state, or process arguments."

🤖 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.test.ts` around lines 304 - 412, Add
negative-path tests for both composeManagedMcpPolicies and
composeDeadlineManagedMcpPolicies covering duplicate currentPolicies keys and a
non-canonical key such as mcp_bridge_Alpha. Assert each composer throws the
expected ambiguous-ownership error before writing the key, and verify error
output does not expose sensitive policy contents or other secrets.

Source: Path instructions

🤖 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 `@src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts`:
- Around line 26-43: Replace the source-text assertions in the lifecycle test
with a behavioral test of the public runSandboxSnapshot entrypoint for kind
"create". Mock withSandboxMutationLock and the backup operation using deferred
promises, start snapshot creation, then verify a competing mutation remains
blocked until the backup promise resolves; also assert the replacement path is
reached through the public entrypoint.

In `@src/lib/shields/flow.test.ts`:
- Around line 515-535: Strengthen the test around createHarness and
applyShieldsPolicySnapshot by inspecting the restored policy body captured in
harness.policySetBodies. Assert that the applied policy contains all 257 managed
MCP policy keys, including keys beyond the 256-item boundary, rather than
checking only the successful status.

In `@src/lib/shields/index.ts`:
- Around line 857-895: Bound retryInlineAutoRestore in src/lib/shields/index.ts
lines 857-895 with a wall-clock deadline or attempt budget, and fail closed
using the last recorded error when recovery cannot complete, including the
missing-snapshot case. Also bound prepareAutoRestoreTransitionTakeover in
src/lib/shields/index.ts lines 936-957 and deduplicate shields_up_failed audit
entries when the message is unchanged.
- Around line 3098-3106: Update the ownerMcpProcessIdentity initialization to
use the bounded fallback provided by the exported
prepareAutoRestoreTransitionTakeover transition-lock flow when
readMcpLockProcessIdentity returns null. Remove the unconditional throw so
shields down can continue on supported hosts where process identity lookup is
unavailable, while preserving the existing process identity path when available.

In `@src/lib/state/mcp-lifecycle-lock-acquisition.ts`:
- Around line 830-841: Update the exactLocalOwner check in the synchronous
reentrancy path to require owner.processIdentity to equal
readMcpLockProcessIdentity(process.pid), rather than merely being non-empty.
Preserve the existing sandbox, host, PID namespace, and PID checks so
recycled-PID records continue through stale-generation handling instead of
throwing NEMOCLAW_SYNC_REENTRANT_OWNER.

In `@src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts`:
- Around line 62-74: The readShieldsTimerMarker function must reject symlinked
marker files instead of following them. Open the marker with fs.openSync using
O_NOFOLLOW, read from that descriptor, and preserve null returns for missing,
invalid, corrupt, or unreadable markers; add a regression test confirming a
symlinked marker yields null.

In `@test/mcp-lifecycle-lock.test.ts`:
- Around line 866-880: The containment test must not assert inside swallowed
onContainment errors. In test/mcp-lifecycle-lock.test.ts lines 866-880, record
ownerPid in a variable, resolve containmentReported, then assert the recorded
value equals child.pid after awaiting the promise. In
test/mcp-lifecycle-lock.test.ts lines 910-912, record each
fs.existsSync(deadlinePath) result in an array within the vi.fn mock, then after
the rejection assertion verify the array is non-empty and every entry is true.

---

Outside diff comments:
In `@test/e2e/support/mcp-bridge-sandbox.test.ts`:
- Around line 305-334: Replace the source-text position and substring checks in
test/e2e/support/mcp-bridge-sandbox.test.ts:305-334 with a focused
public-boundary E2E scenario that performs route removal and asserts the
surviving managed-policy outcome. Replace the source-text assertions in
test/e2e/support/mcp-bridge-sandbox.test.ts:336-380 with a focused Hermes
lifecycle scenario that observes tool-call results before and after Shields,
removal, restart, and rediscovery. Use the existing public test helpers and
assert observable outcomes rather than source structure, private state, or mock
calls.

---

Nitpick comments:
In `@src/lib/shields/index.test.ts`:
- Around line 517-531: Wrap the test exercise and assertions following the
fs.readFileSync and process.kill spy setup in a try/finally block. Store both
spy handles, then call mockRestore() for each in finally so they are restored
even when assertions fail; keep the existing mock behavior and assertions
unchanged.
- Around line 486-494: Update the deadline restore test around
applyShieldsPolicySnapshot to capture the policy file submitted to the gateway
policy-set command and assert its observable applied policy, rather than calling
composeDeadlineManagedMcpPolicies again. Parse the captured YAML and verify
network_policies retains restrictive_baseline while omitting mcp_bridge_alpha,
replacing raw substring assertions and preserving the existing
managedMcpOmissions check.

In `@src/lib/shields/index.ts`:
- Around line 2662-2671: Export the existing TEMP_FILE_PREFIX constant from
permissive-runtime and import it into the code containing the cleanupTempDir
calls. Replace both repeated "nemoclaw-permissive-runtime" literals, including
the call near buildPolicySetCommand and the one near line 3191, with the shared
constant.

In `@src/lib/shields/mcp-policy-transition.test.ts`:
- Around line 304-412: Add negative-path tests for both
composeManagedMcpPolicies and composeDeadlineManagedMcpPolicies covering
duplicate currentPolicies keys and a non-canonical key such as mcp_bridge_Alpha.
Assert each composer throws the expected ambiguous-ownership error before
writing the key, and verify error output does not expose sensitive policy
contents or other secrets.

In `@src/lib/shields/mcp-policy-transition.ts`:
- Around line 11-37: Make src/lib/shields/mcp-policy-transition.ts the single
owner of MCP policy grammar by exporting the reserved-prefix predicate derived
from RESERVED_MANAGED_MCP_POLICY_KEY_RE, along with parsePolicyDocument and
readNetworkPolicies. In src/lib/actions/sandbox/mcp-bridge-policy.ts at line 243
and the additional checks at lines 298-300, 379-381, and 457-459, replace inline
mcp_bridge_ checks with the exported predicate; remove
parseManagedPolicyDocument and readManagedNetworkPolicies at lines 60-83 and use
the exported helpers instead.

In `@src/lib/shields/permissive-runtime.ts`:
- Around line 246-251: Move the readBasePolicy and
composeDeadlineManagedMcpPolicies calls inside the try block in
buildDeadlineRuntimeManagedMcpPolicy, so both failures are wrapped with the same
diagnostic message used by buildRuntimeManagedMcpPolicy while preserving
fail-closed behavior.
- Around line 195-198: Remove the unused basePolicyPath parameter from
buildRuntimeManagedMcpPolicy and buildDeadlineRuntimeManagedMcpPolicy, then
update their call sites in the shields index flow, including the shields-down
path, to match the new signatures. Continue obtaining base policy content
through deps.readBasePolicy().

In `@src/lib/shields/timer-bound-lock.ts`:
- Around line 84-96: Update withTimerBoundAutoRestoreLock to accept an optional
deps: TimerBoundLockDeps parameter defaulting to defaultDeps, and pass that
parameter to withTimerBoundShieldsMutationLockOptions instead of hardcoding
defaultDeps. Preserve the existing call behavior for callers that omit deps.
- Around line 11-18: Remove the six-symbol forwarding export from the shields
helper and update all shields callers, including src/lib/shields/index.ts, to
import these lifecycle lock APIs directly from ../state/mcp-lifecycle-lock.
Preserve the existing API usage while ensuring the state module remains the sole
ownership and import path.

In `@src/lib/shields/timer-control.ts`:
- Around line 219-255: Update killTimer and the KillTimerResult contract to
address the constant terminated: false field: remove it if callers can be
migrated, or explicitly retain and document it for compatibility. Also collapse
the nested verification.warning condition in killTimer into a single guarded
check while preserving warning collection behavior.

In `@src/lib/shields/timer.test.ts`:
- Around line 11-16: Update the test setup around the existing
applyShieldsPolicySnapshot reset to also restore completeAutoRestoreTransition’s
successful implementation for every test. Ensure both mocks are explicitly reset
in the same setup block so restoreMocks cannot leave
completeAutoRestoreTransition returning undefined.
- Around line 75-116: Update invokeTimerAndExpectRetry to use a substantially
longer polling timeout than the current 200 attempts, and track whether the
deadline and audit files become ready. If the wait expires, fail explicitly with
a descriptive timeout message before the exit and deadline assertions; preserve
the existing readiness conditions and retry behavior once the files appear.

In `@src/lib/shields/timer.ts`:
- Around line 514-533: Define named constants for the lifecycle fence poll
interval and timeout alongside AUTO_RESTORE_RETRY_MS, then replace the inline 50
and 5_000 values in the withMcpLifecycleDeadlineFence options within the restore
path with those constants.

In `@src/lib/shields/transition-lock.ts`:
- Around line 493-511: Extract the shared snapshot-reading and owner-projection
logic from inspectShieldsTransitionLockOwner and
inspectAnyShieldsTransitionLockOwner into one private helper that accepts the
differing owner/token predicate. Have both public inspect methods delegate to
this helper, preserving null handling and closeSnapshot cleanup while
centralizing construction of InspectedShieldsTransitionOwner.

In `@src/lib/state/mcp-lifecycle-lock-acquisition.ts`:
- Around line 366-379: Extract shared local helpers for the containment-active
and mutation-lock-timeout diagnostics, and use them in both the asynchronous and
synchronous acquisition paths. Make the helpers produce identical wording,
including the containment details and the timeout’s lowercase “owner pid” suffix
plus the operation-in-progress guidance; update the visible synchronous branch
around readMcpLifecycleLockObservationSync and the corresponding async branch
without changing control flow.

In `@src/lib/state/mcp-lifecycle-lock-storage.ts`:
- Around line 201-234: Add concise contract comments to
reclaimStaleMcpLifecycleLockGenerationSync and
writeMcpLifecycleLockCandidateAndLinkSync, referencing their asynchronous
counterparts. Document the required ordering: atomic rename claim, verify the
moved inode before deletion, restore raced replacements via a non-overwriting
hard link, and decide publication only through LINK plus owner-token
reconciliation. Keep both synchronous implementations aligned with the
asynchronous contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f1b6bb75-a7ae-49c2-98ac-5dc569d708e6

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6828 and dd0a63e.

📒 Files selected for processing (42)
  • ci/env-var-doc-allowlist.json
  • ci/source-architecture-budget.json
  • ci/source-shape-test-budget.json
  • docs/manage-sandboxes/backup-restore.mdx
  • docs/manage-sandboxes/runtime-controls.mdx
  • docs/reference/commands.mdx
  • scripts/checks/openshell-policy-mutation-read.mts
  • src/lib/actions/maintenance.test.ts
  • src/lib/actions/maintenance.ts
  • src/lib/actions/sandbox/mcp-bridge-policy.ts
  • src/lib/actions/sandbox/snapshot-baseline-exclusion-output.test.ts
  • src/lib/actions/sandbox/snapshot-help.test.ts
  • src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts
  • src/lib/actions/sandbox/snapshot.test.ts
  • src/lib/actions/sandbox/snapshot.ts
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.test.ts
  • src/lib/shields/index.ts
  • src/lib/shields/mcp-policy-transition.test.ts
  • src/lib/shields/mcp-policy-transition.ts
  • src/lib/shields/permissive-runtime.ts
  • src/lib/shields/timer-bound-lock.ts
  • src/lib/shields/timer-control.ts
  • src/lib/shields/timer.test.ts
  • src/lib/shields/timer.ts
  • src/lib/shields/transition-lock.test.ts
  • src/lib/shields/transition-lock.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
  • src/lib/state/mcp-lifecycle-lock-identity.ts
  • src/lib/state/mcp-lifecycle-lock-storage.ts
  • src/lib/state/mcp-lifecycle-lock.ts
  • src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts
  • src/lib/state/paths.test.ts
  • src/lib/state/paths.ts
  • test/e2e/live/mcp-bridge-sandbox.ts
  • test/e2e/live/mcp-bridge.test.ts
  • test/e2e/support/mcp-bridge-sandbox.test.ts
  • test/helpers/isolate-test-state.ts
  • test/mcp-lifecycle-lock.test.ts
  • test/permissive-runtime.test.ts
  • test/vitest-temp-root.test.ts
  • vitest.config.ts

Comment thread src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts Outdated
Comment thread src/lib/shields/flow.test.ts Outdated
Comment thread src/lib/shields/index.ts Outdated
Comment thread src/lib/shields/index.ts Outdated
Comment thread src/lib/state/mcp-lifecycle-lock-acquisition.ts
Comment thread src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts
Comment thread test/mcp-lifecycle-lock.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src/lib/shields/timer.test.ts`:
- Around line 103-110: Update the runRestoreTimer test around pending and
markerPath so it waits for the scheduled retry to execute while markerPath
remains removed, rather than relying on await pending alone. Add an assertion
that the retry does not apply the policy a second time, then allow the finally
block to restore markerContents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3917c4b5-ecdb-4847-9a0c-2f3dba12b773

📥 Commits

Reviewing files that changed from the base of the PR and between dd0a63e and 7581fe0.

📒 Files selected for processing (5)
  • src/lib/actions/maintenance.test.ts
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.test.ts
  • src/lib/shields/timer.test.ts
  • test/mcp-lifecycle-lock.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/shields/index.test.ts
  • src/lib/actions/maintenance.test.ts
  • test/mcp-lifecycle-lock.test.ts

Comment thread src/lib/shields/timer.test.ts
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Comment thread src/lib/shields/flow.test.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/lib/shields/index.ts (1)

3169-3254: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Nullish-throw pattern on ownerMcpProcessIdentity still aborts shields down when process identity lookup is unavailable.

This exact segment was flagged in a previous review: readMcpLockProcessIdentity(process.pid, true) ?? (() => { throw new Error("Cannot identify shields-down lifecycle owner process"); })() still throws unconditionally when the identity lookup returns null (for example when /proc is unavailable or ps fails). No "Addressed" marker is attached to that past comment, and the code still matches the pattern it described. Confirm whether the second argument (true) to readMcpLockProcessIdentity already changes this behavior; if not, use the bounded fallback already used by the transition lock (prepareAutoRestoreTransitionTakeover is exported) instead of the unconditional throw, so shields down can proceed on supported hosts where process identity lookup is unavailable.

🤖 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 3169 - 3254, Update the
ownerMcpProcessIdentity initialization in the shields-down transition to avoid
unconditionally throwing when readMcpLockProcessIdentity returns null. Verify
whether its true argument provides the required fallback; otherwise reuse the
bounded fallback behavior exposed by prepareAutoRestoreTransitionTakeover,
allowing shields down to proceed when process identity lookup is unavailable
while preserving transition ownership safety.
🧹 Nitpick comments (1)
src/lib/shields/index.ts (1)

921-971: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated bounded-retry-with-audit-dedup logic.

retryInlineAutoRestore (lines 925-963) and the transition-takeover loop inside withExpiredAutoRestoreDeadlineFence (lines 1013-1040) both implement the same shape: a bounded for loop, try/catch, notifiedError deduplication before appendAuditEntryBestEffort, Atomics.wait between attempts, and escalation to failInteractiveAutoRestoreClosed. Extract a shared helper (e.g., runBoundedInteractiveRetry(attempt => ..., onExhausted)) to avoid the two copies drifting apart as retry semantics evolve.

Also applies to: 973-1063

🤖 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 921 - 971, The bounded retry and
audit-deduplication flow is duplicated between retryInlineAutoRestore and
withExpiredAutoRestoreDeadlineFence. Extract the shared loop behavior into a
helper such as runBoundedInteractiveRetry, including attempt limits, error
deduplication, Atomics.wait delays, and exhaustion handling, then update both
callers to provide only their operation-specific recovery and
failInteractiveAutoRestoreClosed behavior.
🤖 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.

Duplicate comments:
In `@src/lib/shields/index.ts`:
- Around line 3169-3254: Update the ownerMcpProcessIdentity initialization in
the shields-down transition to avoid unconditionally throwing when
readMcpLockProcessIdentity returns null. Verify whether its true argument
provides the required fallback; otherwise reuse the bounded fallback behavior
exposed by prepareAutoRestoreTransitionTakeover, allowing shields down to
proceed when process identity lookup is unavailable while preserving transition
ownership safety.

---

Nitpick comments:
In `@src/lib/shields/index.ts`:
- Around line 921-971: The bounded retry and audit-deduplication flow is
duplicated between retryInlineAutoRestore and
withExpiredAutoRestoreDeadlineFence. Extract the shared loop behavior into a
helper such as runBoundedInteractiveRetry, including attempt limits, error
deduplication, Atomics.wait delays, and exhaustion handling, then update both
callers to provide only their operation-specific recovery and
failInteractiveAutoRestoreClosed behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 61e6a0fe-3d3e-4118-8fbb-81fb6ccd3e83

📥 Commits

Reviewing files that changed from the base of the PR and between 7581fe0 and d61bcbc.

📒 Files selected for processing (14)
  • docs/manage-sandboxes/backup-restore.mdx
  • docs/manage-sandboxes/runtime-controls.mdx
  • docs/reference/commands.mdx
  • src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts
  • src/lib/actions/sandbox/snapshot-restore-test-fixture.ts
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.test.ts
  • src/lib/shields/index.ts
  • src/lib/shields/timer.test.ts
  • src/lib/shields/timer.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
  • src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts
  • test/e2e/support/mcp-bridge-sandbox.test.ts
  • test/mcp-lifecycle-lock.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/manage-sandboxes/backup-restore.mdx
  • src/lib/shields/timer.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
  • src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts
  • src/lib/shields/index.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/lib/shields/flow.test.ts (1)

64-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the policy key from the rendered policy instead of re-deriving it.

managedMcpPolicy hardcodes mcp_bridge_${server}, which duplicates the production key derivation in buildMcpBridgePolicyKey. The current server names (alpha, beta, server0server256) hide any normalization the production helper applies. If that helper changes, YAML.parse(content).network_policies[key] returns undefined and the assertions compare undefined to undefined rather than failing clearly.

Read the key from the rendered document, or import buildMcpBridgePolicyKey.

♻️ Proposed refactor
 function managedMcpPolicy(server: string, address = "8.8.8.8") {
-  const key = `mcp_bridge_${server}`;
   const content = buildMcpBridgePolicyYaml(
     server,
     `https://${server}.example.com/mcp`,
     "hermes-config",
     [address],
   );
-  const networkPolicy = YAML.parse(content).network_policies[key];
-  return { content, networkPolicy, server };
+  const networkPolicies = YAML.parse(content).network_policies as Record<string, unknown>;
+  const [key, networkPolicy] = Object.entries(networkPolicies)[0];
+  return { content, key, networkPolicy, server };
 }

Callers that build key lists can then use policies.map(({ key }) => key).

🤖 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/flow.test.ts` around lines 64 - 74, Update managedMcpPolicy
to obtain the policy key from the parsed rendered document instead of
constructing mcp_bridge_${server} locally. Use the key exposed by
YAML.parse(content).network_policies, and preserve returning the rendered
content, selected networkPolicy, and server.
src/lib/shields/index.test.ts (1)

562-569: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the applied policy instead of recomputing it with the production composer.

Lines 562-569 call composeDeadlineManagedMcpPolicies from the test and then assert the composer's own output. That assertion passes for any composer behavior, so it does not prove what applyShieldsPolicySnapshot applied. Capture the policy file passed to the policy-set command (or the staged runtime YAML) and assert that mcp_bridge_alpha is absent there.

As per path instructions: "Prefer observable outcomes through the public boundary over source-text, private-shape, or mock-call assertions" and "Flag copied production algorithms".

🤖 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 562 - 569, Replace the test’s use
of composeDeadlineManagedMcpPolicies with an assertion against the policy
actually applied by applyShieldsPolicySnapshot. Capture the policy file supplied
to the policy-set command or the staged runtime YAML, then verify it contains
restrictive_baseline and excludes mcp_bridge_alpha without reusing the
production composer.

Source: Path instructions

src/lib/shields/permissive-runtime.ts (1)

195-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both new managed-MCP builders accept an unused basePolicyPath. Each builder reads the base policy only through deps.readBasePolicy(), so the first positional parameter is dead. Callers already track temp-versus-base themselves.

  • src/lib/shields/permissive-runtime.ts#L195-L198: prefix basePolicyPath with _ in buildRuntimeManagedMcpPolicy, or remove it and update the call sites in src/lib/shields/index.ts.
  • src/lib/shields/permissive-runtime.ts#L242-L245: apply the same change in buildDeadlineRuntimeManagedMcpPolicy.

As per coding guidelines: "Prefix intentionally unused variables with _".

🤖 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, Both
managed-MCP builders declare an unused base policy path parameter. In
src/lib/shields/permissive-runtime.ts lines 195-198, rename
buildRuntimeManagedMcpPolicy’s basePolicyPath parameter with an underscore
prefix, and apply the same change to buildDeadlineRuntimeManagedMcpPolicy at
lines 242-245; leave call sites unchanged.

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.

Inline comments:
In `@src/lib/shields/index.ts`:
- Around line 889-915: Bound the retry loop in failInteractiveAutoRestoreClosed
instead of using an unbounded for(;;): track a finite attempt budget or
wall-clock deadline while preserving marker checks and containment-file success
handling. When the budget is exhausted, throw an error containing the last
containment failure and the operator-resolution instruction, so interactive
callers such as shieldsStatus and getShieldsPosture return rather than hanging.
- Around line 3128-3164: Ensure every early return or exception after policy
composition releases a staged temporary policy. Restructure the transition
surrounding policyFile, including saveShieldsState, auto-restore timer setup,
and policy application, under a try/finally that calls
cleanupTempDir(policyFile, "nemoclaw-permissive-runtime") when policyFileIsTemp
remains true and the apply flow has not consumed the file; preserve existing
cleanup behavior without double-cleaning consumed files.

In `@test/mcp-lifecycle-lock.test.ts`:
- Line 381: Clear each deferred marker-rotation timer after the corresponding
expectation completes. At test/mcp-lifecycle-lock.test.ts lines 381, 988, and
1022, retain the handle returned by setTimeout for the 40 ms writeTimerMarker
call and clear it in a finally block surrounding the awaited assertion.

---

Nitpick comments:
In `@src/lib/shields/flow.test.ts`:
- Around line 64-74: Update managedMcpPolicy to obtain the policy key from the
parsed rendered document instead of constructing mcp_bridge_${server} locally.
Use the key exposed by YAML.parse(content).network_policies, and preserve
returning the rendered content, selected networkPolicy, and server.

In `@src/lib/shields/index.test.ts`:
- Around line 562-569: Replace the test’s use of
composeDeadlineManagedMcpPolicies with an assertion against the policy actually
applied by applyShieldsPolicySnapshot. Capture the policy file supplied to the
policy-set command or the staged runtime YAML, then verify it contains
restrictive_baseline and excludes mcp_bridge_alpha without reusing the
production composer.

In `@src/lib/shields/permissive-runtime.ts`:
- Around line 195-198: Both managed-MCP builders declare an unused base policy
path parameter. In src/lib/shields/permissive-runtime.ts lines 195-198, rename
buildRuntimeManagedMcpPolicy’s basePolicyPath parameter with an underscore
prefix, and apply the same change to buildDeadlineRuntimeManagedMcpPolicy at
lines 242-245; leave call sites unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1b2f889e-4302-4af6-9a02-d05ac0239b8d

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6828 and d61bcbc.

📒 Files selected for processing (43)
  • ci/env-var-doc-allowlist.json
  • ci/source-architecture-budget.json
  • ci/source-shape-test-budget.json
  • docs/manage-sandboxes/backup-restore.mdx
  • docs/manage-sandboxes/runtime-controls.mdx
  • docs/reference/commands.mdx
  • scripts/checks/openshell-policy-mutation-read.mts
  • src/lib/actions/maintenance.test.ts
  • src/lib/actions/maintenance.ts
  • src/lib/actions/sandbox/mcp-bridge-policy.ts
  • src/lib/actions/sandbox/snapshot-baseline-exclusion-output.test.ts
  • src/lib/actions/sandbox/snapshot-help.test.ts
  • src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts
  • src/lib/actions/sandbox/snapshot-restore-test-fixture.ts
  • src/lib/actions/sandbox/snapshot.test.ts
  • src/lib/actions/sandbox/snapshot.ts
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.test.ts
  • src/lib/shields/index.ts
  • src/lib/shields/mcp-policy-transition.test.ts
  • src/lib/shields/mcp-policy-transition.ts
  • src/lib/shields/permissive-runtime.ts
  • src/lib/shields/timer-bound-lock.ts
  • src/lib/shields/timer-control.ts
  • src/lib/shields/timer.test.ts
  • src/lib/shields/timer.ts
  • src/lib/shields/transition-lock.test.ts
  • src/lib/shields/transition-lock.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
  • src/lib/state/mcp-lifecycle-lock-identity.ts
  • src/lib/state/mcp-lifecycle-lock-storage.ts
  • src/lib/state/mcp-lifecycle-lock.ts
  • src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts
  • src/lib/state/paths.test.ts
  • src/lib/state/paths.ts
  • test/e2e/live/mcp-bridge-sandbox.ts
  • test/e2e/live/mcp-bridge.test.ts
  • test/e2e/support/mcp-bridge-sandbox.test.ts
  • test/helpers/isolate-test-state.ts
  • test/mcp-lifecycle-lock.test.ts
  • test/permissive-runtime.test.ts
  • test/vitest-temp-root.test.ts
  • vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (32)
  • test/helpers/isolate-test-state.ts
  • src/lib/actions/sandbox/snapshot-restore-test-fixture.ts
  • ci/source-shape-test-budget.json
  • src/lib/actions/maintenance.test.ts
  • src/lib/actions/sandbox/snapshot-baseline-exclusion-output.test.ts
  • src/lib/actions/sandbox/snapshot-help.test.ts
  • src/lib/state/paths.test.ts
  • ci/source-architecture-budget.json
  • src/lib/state/paths.ts
  • src/lib/actions/sandbox/snapshot.test.ts
  • src/lib/state/mcp-lifecycle-lock.ts
  • ci/env-var-doc-allowlist.json
  • docs/reference/commands.mdx
  • scripts/checks/openshell-policy-mutation-read.mts
  • test/vitest-temp-root.test.ts
  • src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts
  • test/e2e/support/mcp-bridge-sandbox.test.ts
  • vitest.config.ts
  • src/lib/shields/transition-lock.ts
  • src/lib/actions/maintenance.ts
  • src/lib/shields/timer-bound-lock.ts
  • src/lib/shields/timer-control.ts
  • src/lib/state/mcp-lifecycle-lock-identity.ts
  • test/e2e/live/mcp-bridge-sandbox.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
  • src/lib/shields/mcp-policy-transition.test.ts
  • src/lib/shields/timer.ts
  • src/lib/shields/mcp-policy-transition.ts
  • src/lib/actions/sandbox/mcp-bridge-policy.ts
  • src/lib/actions/sandbox/snapshot.ts
  • test/e2e/live/mcp-bridge.test.ts
  • docs/manage-sandboxes/backup-restore.mdx

Comment thread src/lib/shields/index.ts Outdated
Comment thread src/lib/shields/index.ts
Comment thread test/mcp-lifecycle-lock.test.ts Outdated
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 127-130: Rewrite the third sentence so NemoClaw is the active
actor that records durable containment when the deadline expires or reaping is
interrupted, while preserving the surviving-descendants condition. Use
professional present-tense second-person wording throughout the changed prose,
addressing the reader as “you.”
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 40b59f74-68e6-414b-a1f5-02dc9d099632

📥 Commits

Reviewing files that changed from the base of the PR and between d61bcbc and 4fa5cbd.

📒 Files selected for processing (9)
  • docs/manage-sandboxes/runtime-controls.mdx
  • docs/reference/commands.mdx
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.test.ts
  • src/lib/shields/index.ts
  • src/lib/shields/permissive-runtime.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
  • test/config-set-nested-ssrf.test.ts
  • test/mcp-lifecycle-lock.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/lib/shields/index.test.ts
  • docs/reference/commands.mdx
  • src/lib/shields/permissive-runtime.ts
  • src/lib/shields/index.ts

Comment thread docs/manage-sandboxes/runtime-controls.mdx Outdated
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

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

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@cv

cv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Security review

Verdict: PASS

I reviewed the complete 42-file diff for commit 0f0987864b97f404b8be94844d2c178bdaeaf68d against base SHA 4cd4d64fe67143b57707f874afa0b9d269dfeff2. I found no security vulnerabilities.

The product scope gate passes. Issue #7952 defines the accepted defect, supported lifecycle behavior, security expectations, and validation criteria. This PR repairs an existing supported surface rather than creating a new integration or product surface.

Category Result Evidence
Secrets and credentials PASS No credentials or hardcoded secrets were added. Malformed URL or credential-shaped material is deliberately excluded from errors.
Input validation and sanitization PASS Managed MCP ownership requires canonical YAML, policy keys, endpoints, sorted unique public address pins, and SSRF validation. Sandbox names and transition tokens are validated before path or authority use.
Authentication and authorization PASS Restore authority requires agreement among the registry, generated-policy record, live policy, timer marker, transition token, and lifecycle-lock generation.
Dependencies PASS No dependency manifest or lockfile changed. The repository dependency-audit checks passed.
Errors and logging PASS Policy staging and restoration fail closed, clean temporary files, and avoid logging credential-shaped URL material.
Cryptography and data protection PASS No cryptographic algorithm changed. Transition and lock ownership tokens are unpredictable, and state is stored with restrictive modes and atomic publication.
Configuration security PASS Restore composition removes saved managed MCP entries and overlays only independently proven current entries. Unproven reserved entries are omitted rather than restored.
Security testing PASS Tests cover private or scoped pins, drift, orphan removal, symlink markers, publication races, deadline containment, replacement preservation, and the Hermes MCP survival journey.
System security PASS No Docker capability, exposed-port, authentication-endpoint, or network-bypass behavior changed. Deadline and lifecycle gates retain fail-closed containment without force-killing active owners.

Key implementation evidence:

All 30 commits in the PR are GitHub-verified with reason valid, the DCO declaration is present, and there are no unresolved review threads.

The required E2E / PR Gate check did not pass for this commit because the child job's OpenShell CLI download ended with curl: (35) Recv failure: Connection reset by peer before product tests ran. This is a repository-gate blocker, not a security finding. Human approval is also still required. This review does not approve or merge the PR.

I did not run local tests; GitHub checks are the validation authority for this review.

Files reviewed

ci/env-var-doc-allowlist.json, ci/source-architecture-budget.json, ci/source-shape-test-budget.json, docs/manage-sandboxes/backup-restore.mdx, docs/manage-sandboxes/runtime-controls.mdx, docs/reference/commands.mdx, scripts/checks/openshell-policy-mutation-read.mts, src/lib/actions/sandbox/mcp-bridge-policy.ts, src/lib/actions/sandbox/snapshot-baseline-exclusion-output.test.ts, src/lib/actions/sandbox/snapshot-help.test.ts, src/lib/actions/sandbox/snapshot-restore-lifecycle.test.ts, src/lib/actions/sandbox/snapshot.test.ts, src/lib/actions/sandbox/snapshot.ts, src/lib/shields/flow.test.ts, src/lib/shields/index.test.ts, src/lib/shields/index.ts, src/lib/shields/mcp-policy-transition.test.ts, src/lib/shields/mcp-policy-transition.ts, src/lib/shields/permissive-runtime.ts, src/lib/shields/timer-bound-lock.ts, src/lib/shields/timer-control.ts, src/lib/shields/timer.test.ts, src/lib/shields/timer.ts, src/lib/shields/transition-lock.test.ts, src/lib/shields/transition-lock.ts, src/lib/state/mcp-lifecycle-lock-acquisition.test.ts, src/lib/state/mcp-lifecycle-lock-acquisition.ts, src/lib/state/mcp-lifecycle-lock-identity.ts, src/lib/state/mcp-lifecycle-lock-storage.ts, src/lib/state/mcp-lifecycle-lock.ts, src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts, src/lib/state/paths.test.ts, src/lib/state/paths.ts, test/config-set-nested-ssrf.test.ts, test/e2e/live/mcp-bridge-sandbox.ts, test/e2e/live/mcp-bridge.test.ts, test/e2e/support/mcp-bridge-sandbox.test.ts, test/helpers/isolate-test-state.ts, test/mcp-lifecycle-lock.test.ts, test/permissive-runtime.test.ts, test/vitest-temp-root.test.ts, and vitest.config.ts.

@jyaunches

Copy link
Copy Markdown
Contributor Author

Superseded by the reviewable split requested here: #8130 contains the generic Shields lifecycle/deadline safety prerequisite, and #8141 contains the focused managed-MCP policy reconciliation plus the corrected Hermes regression order. #8141 is stacked on #8130. Closing this oversized combined PR in favor of those two scoped changes.

@jyaunches jyaunches closed this Aug 3, 2026
cv pushed a commit that referenced this pull request Aug 3, 2026
<!-- markdownlint-disable MD041 -->
## Summary

`nemoclaw shields down` replaced the complete live OpenShell policy and
dropped generated policy entries for registered Model Context Protocol
(MCP) servers. This change reconciles only exact NemoClaw-managed MCP
entries during Shields transitions, so a surviving server remains
reachable while removed servers stay removed.

Stacked on prerequisite #8130, which makes Shields deadline recovery
serialize with lifecycle mutations without signaling the lock owner,
this focused fix supersedes the MCP portion of #7980.

## Related Issue

Fixes #7952

## Changes

- Prove managed MCP policy ownership from exact agreement between the
sandbox registry, committed generated-policy record, and live gateway
policy.
- Save the owned MCP key manifest with the Shields snapshot, remove
snapshot-time managed entries during restoration, and overlay only
current exact entries.
- Fail closed on ambiguous, stale, incomplete, malformed, or legacy
ownership during manual transitions. At an expired deadline, omit
unproven managed MCP entries and audit the omission instead of extending
the Shields-down window.
- Preserve current managed MCP entries when building the permissive
runtime policy, while rejecting an unreadable or ambiguous live policy.
- Clean staged runtime policy files across early failure paths.
- Restore the Hermes live regression assertions at the actual failure
boundary and around the unrelated server lifecycle.
- Document MCP policy reconciliation for manual and automatic
restoration.

## Failure Timing and Hermes Upgrade Context

The original journey had a hidden Shields lifecycle between the first
successful call to server A and the later lifecycle for server B:

1. Run `shields up`.
2. Restart the Hermes gateway.
3. Run `shields down`.
4. Exercise the configuration rollback path.
5. Add and remove B.
6. Call A.

Boundary instrumentation recorded in #7952 showed that 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 was removed was only where the test noticed
the already-broken route; B removal was a misleading correlation.

This surfaced during the Hermes upgrade work because new coverage and
upgrade repairs landed nearly back-to-back:

- #7761 added the Hermes MCP helper containing Shields up, gateway
restart, Shields down, and rollback. Its verification collected and
imported the live target but did not run the complete live E2E.
- #7771 upgraded Hermes the next day, but its selected E2Es skipped the
`mcp-bridge` target.
- #7849 repaired Hermes 0.19 migrations and updated MCP tool naming,
allowing the live test to progress far enough to expose the later
failure.
- #7866 moved the explicit `mcp restart A` before the first post-removal
call. Restart reapplied A's generated policy and masked the
missing-policy state.

The corrected regression order is:

1. Run `shields up`.
2. Restart the Hermes gateway.
3. Run `shields down`.
4. Call A immediately.
5. Exercise the configuration rollback path.
6. Add B, prove the DNS-rebinding connection is denied, remove B, and
verify that A's managed policy is unchanged while B's policy is gone.
7. Call A before the later explicit restart.
8. Capture the authenticated rediscovery offset.
9. Run `mcp restart A` without resupplying the secret.
10. Call A and verify authenticated rediscovery.

Whole-policy Shields replacement and the filesystem-only runtime merge
predate the Hermes upgrade. This is a latent NemoClaw Shields
policy-composition defect detected by expanded Hermes regression
coverage, not a Hermes upgrade regression.

## Type of Change

- [ ] Code change (feature, bug fix, or refactor)
- [x] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [ ] Existing tests cover changed behavior — justification:
- [ ] Tests not applicable — justification:
- [x] Docs updated for user-facing behavior changes
- [ ] Docs not applicable — justification:
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: Independent exact-head
Codex security review passed all nine categories at
`18039569796d6ac7604de032edb7abf84f2c73c4`; no findings.
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Documentation Writer Review

- [x] Documentation writer subagent reviewed the completed changes
- Result: `docs-updated`
- Evidence: Reviewed `docs/manage-sandboxes/runtime-controls.mdx` and
`docs/reference/commands.mdx`, all rendered guide variants, changed
operator-facing text, comments, test titles, and the Hermes E2E
chronology. Verified claims against source, issue #7952, and PRs #7761,
#7771, #7849, and #7866. `npm run docs` completed with 0 errors and 2
existing Fern warnings.
- Agent: Codex Desktop
<!-- docs-review-head-sha: 1803956 -->
<!-- docs-review-agents-blob-sha: 3dd7c24 -->

## DGX Station Hardware Evidence

- [ ] Tested on DGX Station
- Tested commit: Not applicable; `scripts/prepare-dgx-station-host.sh`
is unchanged.
- Station profile/scenario: Not applicable.
- Result: Not applicable.
- Supporting evidence: Not applicable.

## Verification

- [x] PR description includes a `Signed-off-by:` line and every commit
appears as `Verified` in GitHub
- [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or
`npm run validate:pr` passed after refreshing `origin/main` when hooks
were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — command/result or justification:
Focused CLI 123/123, integration 11/11, E2E support 13/13, `npm run
typecheck:cli`, `npm run checks:repository`, test-size guardrail, E2E
semantic phase plans, and serial `npm run test:changed` 674/674 passed.
- [x] Applicable broad gate passed — `npm test` for broad
runtime/test-harness changes; `npm run check` for repo-wide
validation/coverage changes — command/result: [Standard PR CI run
30824992396](https://github.com/NVIDIA/NemoClaw/actions/runs/30824992396)
passed. One inherited 50 ms lifecycle-lock assertion timing flake passed
on the failed-job rerun without a code change.
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only)
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

`npm run docs` passed with 0 errors and 2 existing Fern warnings, so the
warning-free checkbox remains unchecked. No new documentation pages were
added.

Trusted E2E [run
30826792180](https://github.com/NVIDIA/NemoClaw/actions/runs/30826792180)
passed all 10 selected checks: cloud inference, cloud onboard, security
posture, inference routing, MCP bridge, MCP bridge dev, network policy,
onboard repair, onboard resume, and OpenShell credential-generation
window. The primary review advisor reported no findings. Nemotron
completed after retrying a protocol-only failure; its one test warning
requested the exact transition/state ownership-mismatch deadline
regression already present in
`src/lib/shields/policy-transition.test.ts`, which passed.

---
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Documentation, examples, guides, or docs build bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shields down drops registered MCP policies from the effective gateway policy

4 participants