Skip to content

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

Open
jyaunches wants to merge 4 commits into
mainfrom
codex/shields-mcp-policy-reconciliation-7952-main
Open

fix(shields): preserve managed MCP policies#8238
jyaunches wants to merge 4 commits into
mainfrom
codex/shields-mcp-policy-reconciliation-7952-main

Conversation

@jyaunches

@jyaunches jyaunches commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

  • Prove managed MCP ownership from the registry, generated-policy record, and live gateway policy before preserving an entry.
  • Save the managed-key manifest with the Shields snapshot, remove snapshot-time managed entries during restoration, and overlay only the current exact entries.
  • Fail closed for ambiguous, malformed, or manually edited ownership; deadline restoration omits unproven entries and records the omission count.
  • Preserve exact current managed entries in the permissive Shields policy without copying unrelated live egress.
  • Sanitize untrusted registry and policy identifiers before including them in operator diagnostics.
  • Clean staged policy files across success and failure paths.
  • Reuse the validated unchanged snapshot when both saved and current managed MCP sets are empty, so deadline restoration does not depend on temporary staging.
  • Update the Hermes MCP regression so it calls A immediately after Shields down and again after B removal, before the later explicit mcp restart A coverage.
  • Document managed MCP policy behavior during Shields transitions.

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:

  1. Raise Shields.
  2. Restart the Hermes gateway.
  3. Lower Shields.
  4. Exercise config rollback.
  5. Add and remove B.
  6. Call A.

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:

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

  1. Raise Shields and restart the Hermes gateway.
  2. Lower Shields and call A immediately.
  3. Exercise config rollback.
  4. Add B, prove DNS-rebinding access is denied, remove B, and verify A's policy is unchanged while B is gone.
  5. Call A before the later explicit restart.
  6. Capture authenticated rediscovery state, restart A without resupplying its secret, and call A again.

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 maintainer security review of commit 5d46d4a2ee924edf743ac36d04807948eec01c96 passed all nine categories. The review covered managed-policy ownership, diagnostic sanitization, deadline restoration, empty-MCP staging failure, and current-main integration.
  • 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: Reviewed the complete PR change at commit 5d46d4a2ee924edf743ac36d04807948eec01c96 against 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.
  • Agent: Codex Desktop

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

  • 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 — command/result or justification: GitHub CI is running for commit 5d46d4a2ee924edf743ac36d04807948eec01c96.
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result: GitHub CI is running for commit 5d46d4a2ee924edf743ac36d04807948eec01c96.
  • 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) — GitHub CI is running for commit 5d46d4a2ee924edf743ac36d04807948eec01c96.
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only) — no new pages

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

Summary by CodeRabbit

  • New Features

    • Shields transitions now securely track and reconcile managed MCP policies.
    • Existing MCP servers retain verified endpoints and address pins during unlock and restoration.
    • Removed MCP servers remain removed instead of being unintentionally restored.
    • Automatic restoration omits policies that cannot be independently verified and records clear warnings.
  • Bug Fixes

    • Improved fail-closed behavior for malformed, mismatched, incomplete, or unavailable policy data.
  • Documentation

    • Expanded guidance on MCP policy handling during manual and automatic Shields transitions.

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
(cherry picked from commit 1803956)
(cherry picked from commit 57e27a1a1b2df4bf16d52aeec078ebf6ec68d793)
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Managed MCP Shields lifecycle

Layer / File(s) Summary
Managed MCP inspection and composition
src/lib/actions/sandbox/mcp-bridge-policy.ts, src/lib/shields/mcp-policy-transition.ts, src/lib/shields/mcp-policy-transition.test.ts
Validates managed MCP ownership and gateway state. Composes current policies with snapshot keys and records omissions.
Permissive runtime and ownership capture
src/lib/shields/permissive-runtime.ts, src/lib/shields/index.ts, test/helpers/shields-flow-harness.ts, src/lib/shields/flow.test.ts, test/permissive-runtime.test.ts
Preserves managed policies during Shields down. Persists exact keys and fails closed when managed policy data cannot be read or staged.
Deadline restoration and audit
src/lib/shields/index.ts, src/lib/shields/timer.ts, src/lib/shields/*test.ts, scripts/checks/openshell-policy-mutation-read.mts
Restores snapshots through applyShieldsPolicySnapshot. Omits unproven entries during authoritative recovery and records warnings.
MCP lifecycle verification
test/e2e/live/mcp-bridge-sandbox.ts, test/e2e/live/mcp-bridge.test.ts, test/e2e/support/mcp-bridge-sandbox.test.ts, test/e2e/live/mcp-bridge-hermes-lifecycle.ts
Captures managed policy state and verifies surviving endpoints, address pins, policy removal, and Hermes lifecycle behavior.
Documentation and architecture controls
docs/manage-sandboxes/runtime-controls.mdx, docs/reference/commands.mdx, ci/source-architecture-budget.json
Documents managed MCP transition rules and lowers the configured fanIn maximum from 89 to 88.

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
Loading

Possibly related issues

Possibly related PRs

Suggested labels: area: docs

Suggested reviewers: cv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.26% 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
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.
Linked Issues check ✅ Passed The implementation, tests, E2E updates, and documentation address the linked issue's MCP preservation and stale-restoration objectives [#7952].
Out of Scope Changes check ✅ Passed The changes support MCP policy preservation, Shields lifecycle safety, testing, documentation, and related maintenance without evident unrelated scope.
✨ 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/shields-mcp-policy-reconciliation-7952-main

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@github-code-quality

github-code-quality Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit 5d46d4a in the codex/shields-mcp-po... branch remains at 96%, unchanged from commit 0d1cb93 in the main branch.


Updated August 04, 2026 22:24 UTC

@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: 2

🧹 Nitpick comments (7)
src/lib/shields/timer.test.ts (1)

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

Type the mock return with the production ManagedMcpPolicyOmission type.

The local type requires server on every omission. The production type in src/lib/actions/sandbox/mcp-bridge-policy.ts (lines 41-46) makes server, key, and policyName all optional and requires only reason. Real omissions frequently carry no server, including the orphan registration case and the ownership-mismatch records that applyShieldsPolicySnapshot pushes.

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.hoisted runs before imports, so use a top-level import type only. 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 win

Share one definition of the reserved MCP key namespace and the policy parse helpers.

RESERVED_MANAGED_MCP_POLICY_KEY_RE here and key.startsWith("mcp_bridge_") in src/lib/actions/sandbox/mcp-bridge-policy.ts (lines 246, 264, 302) define the same security-relevant namespace twice. parsePolicyDocument and readNetworkPolicies also duplicate parseManagedPolicyDocument and readManagedNetworkPolicies in 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 win

Capture the applied policy through buildPolicySetCommand instead of fs.rmSync.

This test reads the staged policy by intercepting fs.rmSync and reading the temp directory before removal. That works only while cleanupTempDir uses synchronous fs.rmSync on the temp parent. A switch to fs.promises.rm would leave appliedPolicy empty, and the assertions at lines 587-588 would then pass or fail for the wrong reason.

src/lib/shields/flow.test.ts and src/lib/shields/policy-transition.test.ts both capture the body inside a buildPolicySetCommand mock. 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 value

Replace the boolean Map and boolean switch with plain conditionals.

readFileWithUnreadableRegistry builds a two-entry Map keyed by true/false, then immediately looks up one key and asserts non-null. readRuntimePolicyBeforeCleanup switches over a boolean expression. Both are indirect forms of a single if. 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 value

Confirm the rollback restore still matches persisted state.

rollbackShieldsDown now calls applyShieldsPolicySnapshot without options. That call requires state.shieldsPolicySnapshotPath === snapshotPath, otherwise it throws "Shields state does not match the policy snapshot being restored". Both rollback call sites run after saveShieldsState persists shieldsPolicySnapshotPath, 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 saveShieldsState does 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 win

Add 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 basePermissivePath fallback would silently drop every managed MCP entry, which is the exact #7952 failure.

💚 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 value

Drop the unused _basePolicyPath parameter, and wrap the deadline read error.

Two small consistency items in the new public API:

  1. buildRuntimeManagedMcpPolicy and buildDeadlineRuntimeManagedMcpPolicy never use _basePolicyPath. Callers in src/lib/shields/index.ts still pass snapshotPath or basePath, which suggests the argument affects the result. readBasePolicy is the only source of the base policy.
  2. buildRuntimeManagedMcpPolicy wraps a readBasePolicy failure with a descriptive message. buildDeadlineRuntimeManagedMcpPolicy at line 246 lets the raw error escape, so the deadline audit records a bare EACCES instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8683777 and d085384.

📒 Files selected for processing (20)
  • ci/source-architecture-budget.json
  • 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/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/policy-transition.test.ts
  • src/lib/shields/timer.test.ts
  • src/lib/shields/timer.ts
  • test/e2e/live/mcp-bridge-hermes-lifecycle.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/shields-flow-harness.ts
  • test/permissive-runtime.test.ts
💤 Files with no reviewable changes (1)
  • test/e2e/live/mcp-bridge-hermes-lifecycle.ts

Comment thread docs/manage-sandboxes/runtime-controls.mdx Outdated
Comment thread src/lib/shields/flow.test.ts Outdated
@github-actions

github-actions Bot commented Aug 4, 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): Completed · high confidence · 0 blockers · 0 warnings · 0 suggestions
  • Model comparison: normalized findings match; normalized terminology decisions differ; normalized E2E selections differ; severity counts match.
7 terminology differences from the second opinion

Advisory only. These are normalized differences from the primary terminology receipt.

  • canonical at src/lib/actions/sandbox/mcp-bridge-policy.ts:86: selected only by the second-opinion lane as justified.
  • unproven at docs/manage-sandboxes/runtime-controls.mdx:136: selected only by the second-opinion lane as justified.
  • ownership manifest at src/lib/shields/mcp-policy-transition.ts:72: selected only by the second-opinion lane as justified.
  • deadline composition at src/lib/shields/mcp-policy-transition.ts:95: selected only by the second-opinion lane as define.
  • legacy Shields at src/lib/shields/mcp-policy-transition.ts:173: selected only by the second-opinion lane as define.
  • provable at src/lib/actions/sandbox/mcp-bridge-policy.ts:48: selected only by the second-opinion lane as define.
  • reserved MCP policy key at src/lib/actions/sandbox/mcp-bridge-policy.ts:249: selected only by the second-opinion lane as define.
4 additional E2E selections from the second opinion

Advisory only. The primary lane did not select these E2E jobs or targets.

  • hermes-shields-config: The completed second-opinion lane identified E2E coverage that the primary lane omitted.
  • shields-config: The completed second-opinion lane identified E2E coverage that the primary lane omitted.
  • sandbox-operations: The completed second-opinion lane identified E2E coverage that the primary lane omitted.
  • state-backup-restore: The completed second-opinion lane identified E2E coverage that the primary lane omitted.

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 decisions

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

  • established — managed MCP policy at docs/manage-sandboxes/runtime-controls.mdx:133: Use "managed MCP policy" for the generated policy entry and "managed MCP" for the broader mediated access concept.
  • define — exact agreement at docs/reference/commands.mdx:1195: Define the equality criteria on first use as matching canonical ownership and policy content across the three named records.

E2E guidance

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

Recommended E2E: cloud-inference, cloud-onboard, managed-image-multiarch-startup, security-posture, inference-routing, mcp-bridge, mcp-bridge-dev, network-policy, onboard-repair, onboard-resume

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>
@wscurran wscurran added area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery bug-fix PR fixes a bug or regression labels Aug 4, 2026

@cv cv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

cv added 2 commits August 4, 2026 14:57
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@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.

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 win

Document the buffer limit per stream. maxBuffer applies separately to the piped stdout and stderr streams, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 96ebaf7 and 5d46d4a.

📒 Files selected for processing (4)
  • ci/source-architecture-budget.json
  • docs/reference/commands.mdx
  • src/lib/shields/index.test.ts
  • src/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

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

Labels

area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery bug-fix PR fixes a bug or regression v0.0.103 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shields down drops registered MCP policies from the effective gateway policy

3 participants