Skip to content

fix(shields): keep registered network routes when shields down swaps policy - #7963

Open
harjothkhara wants to merge 7 commits into
NVIDIA:mainfrom
harjothkhara:fix/7952-shields-down-network-routes
Open

fix(shields): keep registered network routes when shields down swaps policy#7963
harjothkhara wants to merge 7 commits into
NVIDIA:mainfrom
harjothkhara:fix/7952-shields-down-network-routes

Conversation

@harjothkhara

@harjothkhara harjothkhara commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

shields down replaced the whole live OpenShell policy with the static permissive baseline. That baseline is a fixed network allowlist, so a route registered at runtime — a generated MCP bridge entry — was dropped from the effective gateway policy. The server stayed in the registry with its provider attached and its adapter registered, but the gateway no longer served it, so tool discovery and tool calls failed until mcp restart reapplied the generated policy.

buildRuntimePermissivePolicy already unions the live sandbox's filesystem paths into that baseline for the same reason. This extends it to network_policies, so live-only routes survive the swap.

Related Issue

Refs #7952. This fixes the shields-down direction only — see Scope below.

Changes

  • src/lib/shields/permissive-runtime.ts — union the live policy's network_policies into the permissive document. The static base wins on a shared key, so every route the baseline declares is applied byte-for-byte as before. The "nothing to merge" early return now also considers network routes.
  • Provider-composed _provider_* entries are never copied. policy get --base can include them defensively and policy set must not receive them; every other read-modify-write path filters them via withoutProviderComposedPolicies. The prefix is inlined rather than imported because that module is only reachable through a built artifact and this helper is dependency-free.
  • src/lib/shields/index.ts — comment only, at the call site.
  • Tests below.

No new configuration, flag, or abstraction.

Scope

Deliberately not in this PR, so a reviewer is not surprised:

  • The restore direction is unchanged. shields up, inline expiry recovery, the auto-restore handoff, rollback, and the detached timer still reapply the captured snapshot verbatim (src/lib/shields/index.ts:2229, :2341, :2400, :2508, and src/lib/shields/timer.ts:294). A route registered during the shields-down window is still discarded on restore. That is acceptance criterion 2, and it is a separate change on purpose: a correct restore cannot union the permissive document into the snapshot — that would carry permissive-only baseline entries into the locked posture. It needs registry-scoped reconciliation of additions and removals, which is a decision about what may enter lockdown. It also lands in the two files open PR refactor(architecture): collapse runtime cycle to policy seam #7749 is refactoring.

    Sequenced follow-up: Shields restoration reapplies a stale policy snapshot and drops routes registered during the window #8237 owns this half. It names the five restoration call sites, records why the naive union is unsafe (it would carry permissive-only baseline entries into the locked posture), lists the open design questions, and carries the live Hermes MCP qualification — including that the current lane runs mcp restart A before proving A works, which masks the defect.

  • Narrowed to the registered consumer. A live route is carried only when the registry records it for that sandbox; an unregistered live route is left behind. registeredNetworkPolicyKeys reads the registry through the existing policy seam, so no module gained a dependency — the architecture budget reports the same 4503 edges before and after. Note the registry slot holds both generated MCP bridge policies and custom presets added with policy add --from-file; both are registered consumers and both are preserved.

  • shields down --policy <path> is unchanged. An explicitly supplied policy file is still applied verbatim.

  • The I/O fallback degrades, and now says so. If the baseline cannot be read or parsed, or the merged policy cannot be written, the static policy is applied and owned routes are lost — aborting shields down would strand the sandbox in a locked posture. That degrade is deliberate and documented on the helper, and it is no longer silent: the command names the routes that were dropped and the command that restores each kind. Four tests pin it, including that nothing is reported when no owned route was at stake.

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: no page documents this failure mode or a mcp restart workaround for it, and no command, flag, default, or documented contract changes. The fix restores behavior the docs already assume.
  • 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: requesting maintainer review. The change only adds routes to the permissive (unlocked) posture, and only routes the stricter pre-transition policy already authorized. Nothing is added to the locked posture.
  • 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: no-docs-needed
  • Evidence: no files changed. Re-reviewed for commit 8c80c7653, which adds operator-visible warning output. Checked docs/reference/troubleshoot-mcp-servers.mdx, docs/reference/troubleshooting.mdx (shields entries and the network-policy denial precedent), docs/manage-sandboxes/runtime-controls.mdx, docs/reference/commands.mdx (the shields and policy add entries), docs/deployment/set-up-mcp-bridge.mdx, docs/manage-sandboxes/manage-mcp-servers.mdx, and docs/security/tcb-boundary.mdx (whose own rule asks reviewers to re-check when the shields coordinator changes). No page states or implies the old behavior. docs/.docs-skip excludes both openclaw-sandbox-permissive.yaml and the permissive-mode concept from generated content, so documenting the merge itself is a maintainer scope decision rather than a bug-fix change; the warning names its own remediation.
  • Agent: Claude Code

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: see Proof below
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result:
  • 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)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Proof

The red comes from the real shieldsDown path, not the helper alone. src/lib/shields/flow.test.ts drives the exported shieldsDown with only the process boundary stubbed, and reads the document handed to openshell policy set at call time.

Against upstream sources, with a live policy carrying mcp_bridge_fake and a baseline carrying nvidia:

 FAIL  |cli| src/lib/shields/flow.test.ts > shields command flow > shieldsDown keeps a registered MCP route in the policy it applies (#7952)
AssertionError: expected 'version: 1\nnetwork_policies:\n  nvid…' to contain 'mcp_bridge_fake'

- Expected
+ Received

- mcp_bridge_fake
+ version: 1
+ network_policies:
+   nvidia: {}

The applied document is the bare baseline. The route is gone.

Helper-level red, same commit, using the real generated policy from buildMcpBridgePolicyYaml rather than hand-written YAML:

 Test Files  1 failed (1)
      Tests  3 failed | 11 passed (14)

After the fix:

$ npx vitest run --project cli src/lib/shields/
 Test Files  20 passed (20)
      Tests  310 passed (310)

$ npx vitest run --project integration test/permissive-runtime.test.ts \
    test/hermes-mcp-shields-order.test.ts test/mcp-restart-policy-order.test.ts \
    test/mcp-policy-key-ownership.test.ts test/mcp-add-crash-consistency.test.ts \
    test/rebuild-shields-auto-unlock.test.ts test/rebuild-shields-window.test.ts \
    test/snapshot-shields-guard.test.ts test/shields-deferred-exit-lock.test.ts \
    test/repro-2681-group-writable.test.ts
 Test Files  11 passed (11)
      Tests  90 passed (90)

$ npm run typecheck:cli
(clean)

A second commit adds a behavior parity test: the merge and the canonical withoutProviderComposedPolicies are driven with the same routes and must keep the same set, so the inlined _provider_ prefix cannot drift from the canonical filter unnoticed. It fails if either side changes.

Blast radius was taken by symbol, not by directory: buildRuntimePermissivePolicy has one production caller (src/lib/shields/index.ts:2665) and three test files (test/permissive-runtime.test.ts, src/lib/shields/flow.test.ts, src/lib/shields/legacy-hermes-compat.test.ts). The integration project is excluded from npm run test:changed, so those files were run explicitly.

Limits

  • The live Hermes mcp-bridge E2E (acceptance criterion 6) was not run. It needs Brev, which I do not have. test/e2e/live/mcp-bridge.test.ts still calls mcp restart A before proving A works, which preserves the masking behavior the issue describes; proving A across B's lifecycle without that restart needs the live lane.
  • test/e2e/support/lifecycle-user-service.test.ts fails on this machine, but it fails identically on pristine origin/main with none of this diff applied. Not caused by this change.

Signed-off-by: harjoth harjoth.khara@gmail.com

Summary by CodeRabbit

  • Bug Fixes

    • Runtime permissive policies now preserve live network routes, including MCP bridge routes.
    • Baseline routes take precedence when names overlap.
    • Invalid or missing policy data continues to fall back safely.
    • Provider-generated entries are excluded from merged policies.
    • Policies with non-mapping baseline data are handled safely.
    • Static fallback paths remain available when live policy data cannot be used.
  • Documentation

    • Clarified how live filesystem, network, and runtime-injected policy paths are combined.

@copy-pr-bot

copy-pr-bot Bot commented Jul 31, 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.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime permissive policy now merges live network routes with the static baseline. Provider-composed entries are excluded, baseline definitions take precedence for duplicate names, and Shields-down tests verify preservation of live MCP routes.

Changes

Runtime policy merge

Layer / File(s) Summary
Merge live runtime policies
src/lib/shields/permissive-runtime.ts, src/lib/shields/index.ts
The runtime policy merge includes live network routes, preserves baseline definitions, excludes _provider_* entries, and updates the related documentation.
Validate network-route merging
test/permissive-runtime.test.ts
Tests cover route merging, duplicate precedence, provider filtering, malformed inputs, and non-mapping baseline policies.
Verify Shields-down policy application
src/lib/shields/flow.test.ts
The harness accepts configurable policy YAML, captures applied policies, and verifies preservation of live MCP and baseline routes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant shieldsDown
  participant buildRuntimePermissivePolicy
  participant LivePolicyFiles
  participant PolicyApplication
  shieldsDown->>buildRuntimePermissivePolicy: build permissive policy
  buildRuntimePermissivePolicy->>LivePolicyFiles: read live filesystem and network policies
  LivePolicyFiles-->>buildRuntimePermissivePolicy: return policy entries
  buildRuntimePermissivePolicy-->>shieldsDown: return merged policy
  shieldsDown->>PolicyApplication: apply merged policy
  PolicyApplication-->>shieldsDown: preserve live MCP and baseline routes
Loading

Possibly related issues

Possibly related PRs

  • NVIDIA/NemoClaw#6718: Both PRs validate policy behavior through parsed and merged YAML.
  • NVIDIA/NemoClaw#7965: Both PRs concern runtime permissive-policy generation and policy application in Shields flows.

Suggested labels: bug-fix, area: security

Suggested reviewers: cv

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes preserving registered network routes when shields down replaces the live policy.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@harjothkhara
harjothkhara marked this pull request as ready for review July 31, 2026 02:24
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Blocking findings reported

Advisor assessment: Blockers require maintainer review
Next action: Review the blockers below.
Findings: 1 blocker · 0 warnings · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Completed · high confidence · 1 blocker · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Completed · high confidence · 0 blockers · 4 warnings · 0 suggestions
  • Model comparison: normalized findings differ; normalized terminology decisions differ; normalized E2E selections differ; Nemotron reported 1 fewer blocker, 4 more warnings, the same number of suggestions.
1 additional E2E selection from the second opinion

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

  • sandbox-operations: 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.

3 semantic terminology decisions

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

  • established — provider-composed at src/lib/shields/permissive-runtime.ts:45: Keep `provider-composed` for `_provider_*` entries.
  • define — registered network route at src/lib/shields/index.ts:3165: Define the registry-key ownership criterion near the first user-visible warning or use `registered network-policy entry` consistently.
  • justified — degrade at src/lib/shields/permissive-runtime.ts:50: Keep `degrade` only with its named fallback condition and lost-route outcome, as this helper does.

E2E guidance

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

Recommended E2E: cloud-inference, security-posture, inference-routing, network-policy

2 optional E2E recommendations
  • mcp-bridge
  • shields-config

Blockers

PRA-1 Blocker — Reject malformed registered policy content before replacing the live policy

  • Location: src/lib/policy/index.ts:2105
  • Category: correctness
  • Problem: `registeredNetworkPolicyKeys` treats malformed registered custom-policy content as an empty key set. Shields down then replaces the live policy without carrying the route, although the registry still records that policy.
  • Impact: A corrupted registry row can cause Shields down to remove a registered MCP or custom-preset route while reporting no degraded merge, leaving the recorded policy unusable until it is reapplied.
  • Fix: Make `registeredNetworkPolicyKeys` report indeterminate registered content, and have Shields down fail before policy replacement or explicitly surface and handle that state without silently dropping the route.
  • Verification: Inspect `registeredNetworkPolicyKeys` and run the focused Shields flow test with malformed `CustomPolicyEntry.content` plus a live matching `network_policies` key.
  • Test coverage: Add a Shields-down flow test with malformed registered custom-policy YAML and a matching live route. Assert that policy replacement does not occur and that the command reports indeterminate ownership.
  • Evidence: src/lib/policy/index.ts:2102-2108 silently skips an entry when `parsePresetPolicyKeysForOwnership` returns `null`. src/lib/policy/index.ts:2118-2139 treats malformed registered content as an ownership error and refuses reconciliation. src/lib/shields/index.ts:3162 passes the resulting key list to the policy replacement merge.

Workflow run details

This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge.

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

🧹 Nitpick comments (1)
test/permissive-runtime.test.ts (1)

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

Add the local issue suffix to the test title.

Update the title to carries across exactly what the canonical provider filter keeps (#7952). This preserves the behavior-oriented wording and follows the repository test-title guideline.

🤖 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` at line 323, Update the test title in the
carries-across canonical provider filter test to append the local issue suffix
“(`#7952`)”, preserving the existing behavior-oriented wording.

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.

Nitpick comments:
In `@test/permissive-runtime.test.ts`:
- Line 323: Update the test title in the carries-across canonical provider
filter test to append the local issue suffix “(`#7952`)”, preserving the existing
behavior-oriented wording.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6ee55f0c-1114-43c9-b909-e765025567e3

📥 Commits

Reviewing files that changed from the base of the PR and between 788f2d4 and 30f6a24.

📒 Files selected for processing (1)
  • test/permissive-runtime.test.ts

@harjothkhara
harjothkhara force-pushed the fix/7952-shields-down-network-routes branch from 30f6a24 to 1d72143 Compare July 31, 2026 02:42
@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 Jul 31, 2026
@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for the PR. This fixes shields-down dropping registered MCP network routes by unioning live network_policies into the permissive baseline. Maintainers will review the policy union logic and test coverage for route preservation.


Related open issues:

…policy

`shields down` replaces the whole live policy with the static permissive
baseline. That baseline is a fixed allowlist, so a route registered at
runtime — a generated MCP bridge entry — is dropped. The server stays in
the registry with its provider attached and its adapter registered, but
the gateway no longer serves it, so tool discovery and tool calls fail
until `mcp restart` reapplies the generated policy.

Union the live `network_policies` into the permissive document, the same
way the helper already unions filesystem paths. The static base wins on a
shared key, so every route the baseline declares is applied unchanged.
Provider-composed `_provider_*` entries are never copied, because
`policy set` must not receive them.

Refs: NVIDIA#7952
Signed-off-by: harjoth <harjoth.khara@gmail.com>
… one

The permissive merge inlines the `_provider_` prefix instead of importing
`withoutProviderComposedPolicies`, which is only reachable through a built
artifact. Nothing kept the two in sync, so a change to the canonical filter
could let provider-composed entries through this write path unnoticed.

Drive the merge and the canonical filter with the same routes and assert
they keep the same set. The test fails if either side drifts.

Refs: NVIDIA#7952
Signed-off-by: harjoth <harjoth.khara@gmail.com>
Base-wins on a shared network_policies key relies on the baseline being the
more permissive of the two, and nothing enforces that. It holds because the
dynamic writers namespace their keys. Say so, so a future dynamic preset
does not reuse a baseline key and get silently ignored.

Refs: NVIDIA#7952
Signed-off-by: harjoth <harjoth.khara@gmail.com>
@harjothkhara
harjothkhara force-pushed the fix/7952-shields-down-network-routes branch from 5900b79 to 8c98627 Compare August 4, 2026 01:31
@apurvvkumaria apurvvkumaria added the v0.0.102 Release target label Aug 4, 2026
@apurvvkumaria apurvvkumaria self-assigned this 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.

Reviewed commit bfe11fe53. The new down-transition test demonstrates the reported route loss, provider-composed entries remain filtered, and baseline entries retain precedence. No secret, injection, authorization, dependency, cryptography, logging, or privilege defect is apparent in that path.

Two supported-contract questions block approval. First, issue #7952 requires shields-up and automatic restoration to reconcile entries created or removed during the shields-down window, plus live Hermes MCP qualification. This PR explicitly leaves that stale-snapshot behavior and the required E2E unresolved. Either complete those criteria here or link an accepted, sequenced follow-up that owns them before treating this slice as independently mergeable. Second, the reproduced consumer is NemoClaw-managed MCP, but the implementation copies every non-provider live network route. That broadens the policy contract without an accepted requirement or registry ownership check. Narrow the merge to the current registered consumer, or provide the accepted product/security decision that authorizes all live routes.

The fallback also silently returns the static policy when parsing or temporary-file I/O fails, which recreates the route-loss defect. Record the intended fail-closed or degraded behavior in the accepted design and protect it with a behavior test. CI for the maintainer-updated commit is now running; the documentation-writer receipt must be refreshed after the new commit.

…merge

Review asked for two contract changes.

Narrow the merge to the registered consumer. The permissive swap now
carries a live network route only when the registry records it for that
sandbox, so an unregistered live route is left behind instead of being
promoted into the applied policy. `registeredNetworkPolicyKeys` reads the
registry through the existing policy seam, so no module gains a dependency.

Stop losing owned routes silently. When the baseline cannot be read or
parsed, or the merged policy cannot be written, shields down still applies
the static policy — aborting would strand the sandbox in a locked posture —
but it now says which routes were dropped and how to restore them.

Refs: NVIDIA#7952
Signed-off-by: harjoth <harjoth.khara@gmail.com>
The degrade warning told the operator to run `mcp restart`, but the
registry slot it reads holds two kinds of entry: generated MCP bridge
policies and custom presets added with `policy add --from-file`. A dropped
custom preset is not restored by `mcp restart`, so the instruction was
wrong for half the cases it can fire on.

Name the routes that were lost and give the command that owns each kind.
Also say "applied policy" rather than naming the permissive document,
matching the vocabulary the docs use.

Refs: NVIDIA#7952
Signed-off-by: harjoth <harjoth.khara@gmail.com>
…-network-routes

Signed-off-by: harjoth <harjoth.khara@gmail.com>

# Conflicts:
#	src/lib/shields/flow.test.ts
@cjagwani

cjagwani commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Follow-up on exact head 8c80c76532874770b6e72171a2514ee0f954b4e8: the new commits resolve two parts of my prior review by limiting preservation to registry-owned policy keys and making degraded read/parse/write behavior explicit with warnings and tests.

The first release-contract blocker remains. Issue #7952 still requires shields-up/automatic restoration to reconcile entries added or removed during the shields-down window and requires the Hermes mcp-bridge live lifecycle on the fix commit. The PR description still explicitly defers both, and I found no accepted sequenced follow-up or maintainer waiver owning those acceptance criteria. The current branch also conflicts with main.

Please complete those criteria here or link an accepted, sequenced follow-up/waiver, then resolve the conflict and refresh exact-head evidence. The existing requested-changes state remains accurate.

@harjothkhara

Copy link
Copy Markdown
Contributor Author

All three addressed on fbf6ac9b1.

Sequencing. #8237 owns the restore leg and the live Hermes qualification, and is linked here.

Breadth. Narrowed in b55ebe3f4: a live route is carried only when the registry records it for that sandbox. Note that slot holds both generated MCP policies and custom presets from policy add --from-file, so both are preserved. Want generated MCP only? I will gate on sourcePath.

Fallback. Kept degraded rather than fail-closed — aborting when /tmp is full leaves the sandbox locked, which seems worse than losing a route. No longer silent: it names the dropped routes and the command that restores each kind, documented and pinned by four tests. Say the word if you want it fail-closed instead.

The first version of that warning said mcp restart, which does not restore a custom preset. Fixed in 8c80c7653.

Docs receipt refreshed; main merged to clear #8130. Not covered here, tracked in #8237: the shields-up direction and the live E2E.

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.102 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants