Skip to content

fix(shields): serialize deadline recovery - #8130

Merged
jyaunches merged 16 commits into
mainfrom
codex/shields-deadline-safety-7952
Aug 4, 2026
Merged

fix(shields): serialize deadline recovery#8130
jyaunches merged 16 commits into
mainfrom
codex/shields-deadline-safety-7952

Conversation

@jyaunches

@jyaunches jyaunches commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Before this change, an expired Shields auto-restore timer could signal a process that still held the sandbox lifecycle lock. Deadline recovery now blocks new mutations, waits for the recorded owner to release its exact lock generation without signaling it, and enters durable containment when safe recovery cannot be proven.

This is the lifecycle/deadline prerequisite for #7952. It does not add NemoClaw-managed MCP network policy composition; that focused fix remains a separate stacked PR.

Related Issue

Part of #7952.

Changes

  • Bind main-owner, reaper, deadline, and containment records to exact host, PID namespace, process-start, inode, timer-token, and filesystem generations.
  • Close ordinary mutation acquisition at restoreAt, wait for verified live owners without consuming the recovery budget, bound recovery to seven attempts, and retain the owning gates after a durable-containment failure.
  • Treat timer-marker removal as cooperative cancellation so a detached timer exits without SIGSTOP or SIGKILL.
  • Route interactive recovery and stopped-container startup-access recovery through the same expired-deadline fence.
  • Hold one per-sandbox lifecycle transaction across stopped-container startup, the Shields window, backup, Shields restoration, and returning the container to its prior stopped state. If the initial mutation lock or Shields unlock cannot be acquired, mark that sandbox as failed and continue. If Shields cannot be restored or stopped-container cleanup fails, abort the remaining backups.
  • Harden the repository audit for policy reads so it verifies the exact function site, base versus full policy view, and failure handling.
  • Isolate deterministic lifecycle-lock tests from host state and document deadline waiting, bounded recovery, durable containment, exact-generation operator recovery, and the complete backup transaction.

Full E2E run 30775326819 on main candidate 98a0e0c5d6bb982ad4d0311ce023670acd2a4dfd exposed the deadline race in shields-config: shields down --timeout 10s was still changing policy and lock state when auto-restore reached restoreAt, and the process received SIGKILL. The same candidate passed scheduled run 30775345807, so the failure was timing-dependent. This PR fixes ownership and recovery instead of increasing the E2E timeout.

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 reviews covered exact-generation authority, live-owner waiting, bounded recovery, retained gates, durable containment, cooperative timer cancellation, and the current-head test-only delta; no policy bypass or unsafe process signaling remains.
  • 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: docs/manage-sandboxes/backup-restore.mdx; docs/manage-sandboxes/runtime-controls.mdx; docs/reference/commands.mdx. Exact-head npm run docs passed with 0 errors and 2 pre-existing hidden-page warnings. Reviewed lifecycle deadline ownership, bounded interactive and detached recovery, durable containment, exact-generation operator guidance, the complete backup transaction, cooperative timer cancellation, startup-access recovery, operator-facing diagnostics, comments, test titles, and rendered OpenClaw, Hermes, and Deep Agents variants. Confirmed this prerequisite makes no managed MCP policy-preservation or composition claim. The exact-head refresh changed only test linearization, one test timeout, and Atomics mock typing; it changed no explanatory text or runtime behavior.
  • 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 — all 16 current PR commits report verified: true with reason valid.
  • 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 — normal hooks passed for the current change set; the final pre-push CLI TypeScript and tag-sync checks passed.
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — Shields flow 26/26, detached timer process 1/1, focused containment regression 1/1, CLI typecheck, Biome, repository checks, source/test size checks, and secret scan passed.
  • Applicable broad gate passed — current-head standard CI and automatically selected E2E are running.
  • 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) — exact-head documentation review found 0 errors and 2 pre-existing hidden-page 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

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
@jyaunches jyaunches added area: docs Documentation, examples, guides, or docs build area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery area: security Security controls, permissions, secrets, or hardening bug-fix PR fixes a bug or regression labels Aug 3, 2026
@jyaunches jyaunches self-assigned this Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 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 deadline-fenced lifecycle locking, durable containment, cooperative Shields recovery, managed MCP policy validation, per-sandbox backup locking, and isolated Vitest state directories. Tests and documentation cover these lifecycle changes.

Changes

Lifecycle containment and recovery

Layer / File(s) Summary
Lifecycle lock and authority foundation
src/lib/state/mcp-lifecycle-lock*, src/lib/shields/transition-lock.ts, src/lib/shields/timer-bound-lock.ts
Lifecycle locks now support synchronous acquisition, deadline fences, takeover tokens, stale-owner controls, exact-generation release, and durable containment.
Managed MCP policy contracts
src/lib/actions/sandbox/mcp-bridge-policy.ts, src/lib/shields/mcp-policy-transition.ts, src/lib/shields/permissive-runtime.ts
Managed MCP policies now use exact ownership validation, deadline-safe omission reporting, restrictive restoration, and fail-closed runtime policy generation.
Shields transition and timer recovery
src/lib/shields/index.ts, src/lib/shields/timer.ts, src/lib/shields/timer-control.ts
Expired timer recovery validates marker authority and owner identity, avoids process termination, retries bounded failures, and retains transition state until completion.
Containment and recovery validation
src/lib/shields/*.test.ts, test/mcp-lifecycle-lock.test.ts, src/lib/state/mcp-lifecycle-lock-acquisition.test.ts
Tests cover active-owner preservation, timer-generation changes, atomic marker handling, policy omissions, retry limits, lock races, and durable containment.
Per-sandbox backup locking
src/lib/actions/maintenance.ts, src/lib/actions/maintenance.test.ts
backup-all now locks setup, backup, relocking, and cleanup separately and reports per-sandbox and aggregate failures.
Vitest state isolation
test/helpers/isolate-test-state.ts, src/lib/state/paths.ts, vitest.config.ts, test/vitest-temp-root.test.ts
Configured Vitest projects now use secure per-process state directories. Plugin and live E2E projects remain outside this setup.
Recovery documentation and E2E integration
docs/manage-sandboxes/*, docs/reference/commands.mdx, test/e2e/*
Documentation and E2E tests describe and validate deadline-gated restoration, exact-generation ownership, durable containment, managed policy preservation, and batch failure handling.

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

Possibly related PRs

  • NVIDIA/NemoClaw#7859: Shares MCP lifecycle-lock acquisition and generation-preserving recovery changes.
  • NVIDIA/NemoClaw#8122: Shares Shields transition-lock handling and corrupt-owner recovery.
  • NVIDIA/NemoClaw#7980: Extends the same Shields, MCP policy, lifecycle-lock, and isolated-test-state areas.

Suggested reviewers: cv, ericksoa

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.49% 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 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 identifies the primary change: serializing Shields deadline recovery.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/shields-deadline-safety-7952

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

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@github-code-quality

github-code-quality Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit eb0184e in the codex/shields-deadli... branch remains at 96%, unchanged from commit 3a7899f in the main branch.

TypeScript / code-coverage/cli

The overall coverage in commit eb0184e in the codex/shields-deadli... branch remains at 81%, unchanged from commit bef5d0b in the main branch.

Show a code coverage summary of the most impacted files.
File main bef5d0b codex/shields-deadli... eb0184e +/-
src/lib/policy/...ne-exclusion.ts 96% 92% -4%
src/lib/actions/maintenance.ts 87% 84% -3%
src/lib/private-networks.ts 93% 90% -3%
src/lib/shields...nsition-lock.ts 87% 86% -1%
src/lib/shields/index.ts 68% 70% +2%
src/lib/state/m...lock-storage.ts 89% 97% +8%
src/lib/domain/.../connect-env.ts 89% 97% +8%
src/lib/state/m...-acquisition.ts 75% 86% +11%
src/lib/shields...imer-control.ts 81% 93% +12%
src/lib/state/m...er-authority.ts 0% 100% +100%

Updated August 04, 2026 15:57 UTC

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: 3

🧹 Nitpick comments (18)
src/lib/actions/maintenance.ts (2)

378-420: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the redundant local state.

result and orphanManifestMessage are initialized to null and then overwritten from attempt. mutationLockFailed duplicates the "mutationLockError" in attempt check. Read the values directly from attempt.

♻️ Proposed simplification
-    let result: sandboxState.BackupResult | null = null;
-    let orphanManifestMessage: string | null = null;
-    let mutationLockError: unknown;
-    let mutationLockFailed = false;
     const attempt = await backupSandboxWithinShieldsWindow(
@@
-    result = attempt.result;
-    orphanManifestMessage = attempt.orphanManifestMessage;
-    if ("mutationLockError" in attempt) {
-      mutationLockError = attempt.mutationLockError;
-      mutationLockFailed = true;
-    }
-    if (mutationLockFailed) {
+    const result = attempt.result;
+    const orphanManifestMessage = attempt.orphanManifestMessage;
+    if ("mutationLockError" in attempt) {
+      const mutationLockError = attempt.mutationLockError;
       const detail =
         mutationLockError instanceof Error ? mutationLockError.message : String(mutationLockError);
       console.error(`  ${RD}✗${R} ${sb.name}: backup failed (mutation lock: ${detail})`);
       failed++;
       return;
     }
🤖 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/actions/maintenance.ts` around lines 378 - 420, Remove the redundant
result, orphanManifestMessage, mutationLockError, and mutationLockFailed locals
around backupSandboxWithinShieldsWindow. Read result and orphanManifestMessage
directly from attempt, and replace the mutationLockFailed branch with an inline
"mutationLockError" in attempt check while deriving the error detail from
attempt.mutationLockError.

257-299: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collapsing the failure-combination cascade.

The block enumerates five explicit combinations of backupError, orphanManifestMessage, relockError, and stoppedContainerCleanupError. Each new failure source doubles the branches. Collect the errors in one array and build a single message from the present causes. That keeps behavior identical and reduces the branch count.

♻️ Sketch of a combined aggregation
const causes: unknown[] = [];
if (hasBackupError) causes.push(backupError);
else if (orphanManifestMessage) causes.push(new Error(orphanManifestMessage));
if (relockError) causes.push(relockError);
if (stoppedContainerCleanupError) causes.push(stoppedContainerCleanupError);
if (causes.length > 1) {
  throw new AggregateError(causes, backupAllFailureMessage(sandboxName, {
    hasBackupError,
    orphanManifest: Boolean(orphanManifestMessage),
    relockFailed: Boolean(relockError),
    cleanupFailed: Boolean(stoppedContainerCleanupError),
  }));
}
if (causes.length === 1) throw causes[0];

Note that the current messages are asserted by tests, so keep the exact strings if you apply this.

🤖 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/actions/maintenance.ts` around lines 257 - 299, Replace the
failure-combination cascade in the maintenance failure-handling block with one
causes collection using backupError or orphanManifestMessage, relockError, and
stoppedContainerCleanupError. Aggregate when multiple causes exist and throw the
sole cause unchanged when only one exists. Preserve the exact currently asserted
error messages by deriving the message from the same combinations of present
failure flags.
src/lib/actions/maintenance.test.ts (3)

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

Assert the reported failure reason.

The test proves that no mutation happens. It does not prove that the sandbox is reported as a mutation-lock failure. backupAll produces backup failed (mutation lock: ...) for this branch, and an accidental reclassification to "could not safely unlock shields" would still pass this test. Capture the console.error spy and assert the reason.

💚 Proposed addition
     vi.spyOn(console, "log").mockImplementation(() => undefined);
-    vi.spyOn(console, "error").mockImplementation(() => undefined);
+    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
@@
     expect(mocks.returnSandboxContainerToStopped).not.toHaveBeenCalled();
+    expect(errorSpy.mock.calls.flat().join("\n")).toContain(
+      "backup failed (mutation lock: Timed out waiting for the sandbox mutation lock)",
+    );
🤖 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/actions/maintenance.test.ts` around lines 277 - 302, Update the test
for backupAll so it captures the console.error spy and asserts the logged
failure includes the expected “backup failed (mutation lock: ...)” reason from
the withSandboxMutationLock rejection. Keep the existing assertions verifying no
container start or backup mutations occur.

833-877: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the loop-abort claim explicit.

The test registers beta in addition to sb-stopped, and toHaveBeenCalledTimes(3) passes only because the AggregateError aborts the loop before beta is attempted. That dependency is implicit. Assert the abort directly, or drop beta if the abort is not part of this test's claim.

💚 Proposed addition
     expect(mocks.withSandboxMutationLock).toHaveBeenCalledTimes(3);
+    expect(mocks.withSandboxMutationLock).not.toHaveBeenCalledWith(
+      "beta",
+      expect.anything(),
+      expect.anything(),
+    );

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/actions/maintenance.test.ts` around lines 833 - 877, Make the test’s
loop-abort expectation explicit around backupAll: verify the second sandbox,
“beta,” is not processed after the cleanup lock failure, using an observable
public outcome rather than relying solely on withSandboxMutationLock call
counts. Keep the existing assertions for the AggregateError and
stopped-container cleanup behavior.

Source: Path instructions


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

Add coverage for a setup failure inside the acquired lock.

The changed tests cover a rejected setup lock. They do not cover the case where the setup interval is entered and then fails. That path in src/lib/actions/maintenance.ts at lines 160-170 starts the stopped container, throws while opening the Shields window, and produces the AggregateError message "Backup setup for '' failed and its started container could not be returned to the stopped state." Nothing in the changed ranges exercises it, so a regression in that branch would ship silently. Add a test that makes openBackupShieldsWindow throw with returnSandboxContainerToStopped returning false.

I can draft that test if you want it.

As per path instructions: "Destructive lifecycle operations must validate before mutation, preserve state/backup invariants, and cover failure, recovery, rebuild, and resume behavior without bypassing the public action boundary."

🤖 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/actions/maintenance.test.ts` around lines 277 - 302, Add a
backupAll() test covering failure after setup enters the acquired mutation lock:
configure startStoppedSandboxContainerForBackup to start a container, make
openBackupShieldsWindow throw, and make returnSandboxContainerToStopped return
false. Assert the operation rejects or exits with the AggregateError message
“Backup setup for '<name>' failed and its started container could not be
returned to the stopped state,” while verifying the started container is not
backed up and the failed restoration path is invoked.

Source: Path instructions

docs/reference/commands.mdx (1)

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

Fix inconsistent capitalization of "Shields."

Lines 2915 and 2918 use "Shields posture" with a capital S. Elsewhere in this file the term is lowercase, for example "shields transitions" in line 1163 and "shields to be down" in the surrounding sections. Use lowercase "shields" here to match the established term for the same concept.

As per coding guidelines, "Use the same term for the same concept. Do not vary terminology for style."

✏️ Proposed fix
-After the copy, it uses a third lock interval with a 30-second acquisition budget to restore the previous Shields posture.
+After the copy, it uses a third lock interval with a 30-second acquisition budget to restore the previous shields posture.
 If the timer expires during the copy, the deadline gate blocks new mutations and waits for the exact backup owner to finish without signaling it.
 An initial lock or unlock failure marks that sandbox as failed, and `backup-all` continues with the next sandbox.
-A failure to restore the previous Shields posture stops `backup-all` before it processes another sandbox.
+A failure to restore the previous shields posture stops `backup-all` before it processes another sandbox.
🤖 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 2913 - 2919, In the backup-all
documentation, update both occurrences of “Shields posture” in the affected lock
and restore descriptions to “shields posture,” preserving the existing wording
and capitalization elsewhere.
docs/manage-sandboxes/backup-restore.mdx (1)

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

Fix inconsistent capitalization of "Shields."

Lines 213, 214, 216, and 219 use "Shields up," "Shields down," and "Shields posture" with a capital S. The rest of this file uses lowercase "shields" as a common noun, for example "requires shields to be down" in line 91 and "the shields auto-restore timer" in line 92. Use lowercase "shields" in these new lines to match the established term for the same concept.

As per coding guidelines, "Use the same term for the same concept. Do not vary terminology for style."

✏️ Proposed fix
-When an eligible sandbox starts with Shields up, `backup-all` acquires the lifecycle lock and opens a 30-minute shields-down window.
-A sandbox that starts with Shields down remains down.
+When an eligible sandbox starts with shields up, `backup-all` acquires the lifecycle lock and opens a 30-minute shields-down window.
+A sandbox that starts with shields down remains down.
 `backup-all` reacquires the lock under that exact timer generation while it copies sandbox state.
-After the copy, it uses a third lock interval with a 30-second acquisition budget to restore the previous Shields posture.
+After the copy, it uses a third lock interval with a 30-second acquisition budget to restore the previous shields posture.
 If the timer expires during the copy, the deadline gate blocks new mutations and waits for the exact backup owner to finish without signaling it.
 An initial lock or unlock failure marks that sandbox as failed, and `backup-all` continues with the next sandbox.
-NemoClaw attempts to restore the previous Shields posture before it processes the next sandbox, including when the backup fails.
+NemoClaw attempts to restore the previous shields posture before it processes the next sandbox, including when the backup fails.
🤖 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/manage-sandboxes/backup-restore.mdx` around lines 213 - 219, In the
backup-all behavior description, update the newly added references in the
sentences around “backup-all” and “NemoClaw” to use lowercase “shields”
consistently, including “shields up,” “shields down,” and “shields posture,”
while preserving the existing meaning and wording.
src/lib/state/mcp-lifecycle-lock-acquisition.ts (3)

506-508: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reset lastOwnerPid when the main lock is absent.

acquireMcpLifecycleLockSync clears lastOwnerPid in the equivalent branch at line 655. The async loop keeps the previous value. The timeout message at line 383 can therefore name an owner PID whose generation already disappeared, which misleads an operator during recovery triage.

♻️ Proposed fix
     } else {
+      lastOwnerPid = null;
       resetCorruptGenerationTracker(corruptMainTracker);
     }
🤖 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 506 - 508,
Update the main-lock-absent branch in the async lifecycle-lock acquisition flow
to clear corruptMainTracker.lastOwnerPid before or alongside
resetCorruptGenerationTracker. Match the equivalent behavior in
acquireMcpLifecycleLockSync while preserving the existing tracker reset logic.

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

Require stateDir in the async fence signature.

acquireDeadlineFenceSync declares options: McpLifecycleDeadlineFenceSyncOptions & { stateDir: string }, so every state-directory read inside it is unambiguous. acquireDeadlineFence does not, and line 846 compensates with options.stateDir ?? resolveNemoclawStateDir() while lines 804, 826, and 796 read options.stateDir directly. The only caller, withMcpLifecycleDeadlineFence, always injects stateDir. Encode that in the type so both fences resolve the state directory in exactly one place.

♻️ Proposed fix
 async function acquireDeadlineFence(
   sandboxName: string,
   takeoverToken: string,
-  options: McpLifecycleDeadlineFenceOptions,
+  options: McpLifecycleDeadlineFenceOptions & { stateDir: string },
 ): Promise<AcquiredMcpLifecycleLock> {
       ensureDurableContainmentForStaleGenerationSync(
         lockPath,
         sandboxName,
-        options.stateDir ?? resolveNemoclawStateDir(),
+        options.stateDir,
         observation,
         "An auto-restore deadline owner exited before its recovery operation completed",
       );
🤖 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 785 - 870,
Update acquireDeadlineFence to require options: McpLifecycleDeadlineFenceOptions
& { stateDir: string }, matching acquireDeadlineFenceSync and the guaranteed
stateDir supplied by withMcpLifecycleDeadlineFence. Remove the fallback
resolveNemoclawStateDir() from ensureDurableContainmentForStaleGenerationSync
and use options.stateDir directly so every state-directory read uses the
required injected value.

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

Document that this constructor mutates the lease.

durableMcpLifecycleContainmentFailure looks like a pure error factory, but line 701 sets retainForDurableContainment on the active lease. That side effect is what keeps the owned generation after the callback returns normally, as the tests at src/lib/state/mcp-lifecycle-lock-acquisition.test.ts lines 394-420 require. A reader who assumes a pure factory could reorder or memoize the call and silently lose gate retention.

Add a doc comment that states the side effect.

♻️ Proposed doc comment
+/**
+ * Build a coded durable-containment failure and mark the currently held
+ * lifecycle lease for retention. The retention flag keeps the exact owned
+ * generation closed even when the caller catches this error, so calling this
+ * function is not side-effect free.
+ */
 export function durableMcpLifecycleContainmentFailure(
   error: unknown,
   lockPath: string,
 ): Error & { code: string } {
🤖 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 696 - 712,
Document durableMcpLifecycleContainmentFailure with a doc comment stating that
it mutates the active lease by setting retainForDurableContainment, preserving
the lease across normal callback completion; make clear that callers must not
treat this error factory as pure.
src/lib/state/mcp-lifecycle-lock-storage.ts (1)

152-156: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Carry the generation-safety rationale into the synchronous reclaim.

The async versions explain two non-obvious invariants: the rename is the atomic claim, and a raced replacement owner must be restored by hard link rather than unlinked. The synchronous mirrors implement the same invariants without that explanation. A future edit to the sync path alone could delete an owner generation it did not claim.

Add a short pointer comment so both paths document the same contract.

♻️ Proposed comment additions
 export function safelyReleaseMcpLifecycleLockSync(lockPath: string, token: string): void {
   const observation = readMcpLifecycleLockObservationSync(lockPath);
   if (!observation || observation.owner?.token !== token) return;
+  // Same contract as safelyReleaseMcpLifecycleLock: claim and verify the
+  // generation before deletion.
   reclaimStaleMcpLifecycleLockGenerationSync(lockPath, observation);
 }
 export function reclaimStaleMcpLifecycleLockGenerationSync(
   targetPath: string,
   expected: LockObservation,
 ): boolean {
+  // Synchronous mirror of reclaimStaleMcpLifecycleLockGeneration. Rename is the
+  // atomic claim; a raced replacement owner is restored by hard link and never
+  // unlinked.
   const quarantinePath = `${targetPath}.reclaim-${process.pid}-${crypto.randomUUID()}`;

Also applies to: 201-234

🤖 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 152 - 156, Add a
concise pointer comment to the synchronous lifecycle-lock release/reclaim path,
covering both safelyReleaseMcpLifecycleLockSync and
reclaimStaleMcpLifecycleLockGenerationSync, that references the async
implementation’s atomic rename claim and hard-link restoration for raced
replacement owners. Do not change the synchronization logic.
src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts (1)

91-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared marker-authority predicate.

readShieldsTimerTakeoverToken and isShieldsTimerDeadlineExpired repeat the same three-part validation: sandbox-name match, processToken type, and the 32-hex pattern. A single helper keeps both authority checks identical if the token format changes later.

♻️ Proposed refactor
+function validatedMarkerToken(
+  marker: ShieldsTimerMarker | null,
+  sandboxName: string,
+): string | undefined {
+  if (
+    marker?.sandboxName !== sandboxName ||
+    typeof marker.processToken !== "string" ||
+    !/^[0-9a-f]{32}$/.test(marker.processToken)
+  ) {
+    return undefined;
+  }
+  return marker.processToken;
+}
+
 export function readShieldsTimerTakeoverToken(
   sandboxName: string,
   stateDir = resolveNemoclawStateDir(),
 ): string | undefined {
-  const marker = readShieldsTimerMarker(sandboxName, stateDir);
-  if (
-    marker?.sandboxName !== sandboxName ||
-    typeof marker.processToken !== "string" ||
-    !/^[0-9a-f]{32}$/.test(marker.processToken)
-  ) {
-    return undefined;
-  }
-  return marker.processToken;
+  return validatedMarkerToken(readShieldsTimerMarker(sandboxName, stateDir), sandboxName);
 }
 
 export function isShieldsTimerDeadlineExpired(
   sandboxName: string,
   stateDir = resolveNemoclawStateDir(),
   now = Date.now(),
 ): boolean {
   const marker = readShieldsTimerMarker(sandboxName, stateDir);
-  if (
-    marker?.sandboxName !== sandboxName ||
-    typeof marker.processToken !== "string" ||
-    !/^[0-9a-f]{32}$/.test(marker.processToken)
-  ) {
-    return false;
-  }
+  if (!validatedMarkerToken(marker, sandboxName) || !marker) return false;
   const restoreAtMs = new Date(marker.restoreAt).getTime();
   return Number.isFinite(restoreAtMs) && restoreAtMs <= now;
 }
🤖 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/shields-timer-authority.ts` around lines 91
- 121, Extract the repeated marker validation from readShieldsTimerTakeoverToken
and isShieldsTimerDeadlineExpired into a shared helper that checks sandbox-name
equality, string processToken type, and the 32-hex token format. Update both
functions to reuse this predicate while preserving their existing return
behavior and deadline validation.
src/lib/shields/timer-bound-lock.ts (1)

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

Accept an injectable deps parameter for symmetry and unit testing.

withTimerBoundShieldsMutationLock and withTimerBoundShieldsMutationLockAsync both accept deps: TimerBoundLockDeps = defaultDeps. withTimerBoundAutoRestoreLock hardcodes defaultDeps, so a unit test cannot inject a fake readToken or withLock to exercise the generation-retry path in isolation. This function guards the auto-restore recovery path, so direct unit coverage is valuable.

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 85 - 97, Update
withTimerBoundAutoRestoreLock to accept an optional deps: TimerBoundLockDeps
parameter defaulting to defaultDeps, and pass that parameter to
withTimerBoundShieldsMutationLockOptions instead of hardcoding defaultDeps,
matching the injectable dependency pattern of the related lock functions.
src/lib/shields/timer.test.ts (1)

92-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Raise the vi.waitFor timeout for CI headroom.

The 200 ms timeout must cover deadline-fence acquisition, the first failed restore attempt, and the audit write, all of which touch the filesystem. On a loaded CI runner this budget is tight and can produce intermittent failures. The 1 ms interval already makes the wait exit as soon as both files appear, so a larger timeout does not slow the passing case.

Raise timeout to about 2000 ms.

♻️ Proposed change
-      { interval: 1, timeout: 200 },
+      { interval: 1, timeout: 2_000 },
🤖 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 92 - 100, Increase the timeout in
waitForRetryBoundary’s vi.waitFor options from 200 ms to approximately 2000 ms,
while keeping the 1 ms polling interval and file-existence assertions unchanged.
src/lib/shields/index.ts (3)

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

Report progress while the inline retry loop blocks.

This loop can block the command for about 30 seconds across 7 attempts, because Atomics.wait blocks the main thread for INTERACTIVE_AUTO_RESTORE_RETRY_MS between attempts. shields status is an interactive command. During the wait it writes nothing to the terminal, so the command appears hung.

Write one console.error line before each wait to state that recovery is retrying behind the lifecycle gate. The failure paths already audit to the log, but the operator at the terminal receives no signal until the loop ends.

♻️ Proposed change
     if (attempt + 1 < INTERACTIVE_AUTO_RESTORE_MAX_ATTEMPTS) {
+      console.error(
+        `  Recovery: auto-restore did not complete; retrying behind the lifecycle gate (attempt ${String(
+          attempt + 2,
+        )} of ${String(INTERACTIVE_AUTO_RESTORE_MAX_ATTEMPTS)}).`,
+      );
       Atomics.wait(transitionPollBuffer, 0, 0, INTERACTIVE_AUTO_RESTORE_RETRY_MS);
     }
🤖 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 937 - 987, Update
retryInlineAutoRestore so that, immediately before each Atomics.wait call, it
writes one console.error line indicating inline auto-restore is retrying behind
the lifecycle gate. Keep the message in the existing retry condition and
preserve the current wait behavior and audit logging.

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

Remove the duplicated transition read and snapshot check.

inspectAutoRestoreTransitionTakeoverOwner at Line 2495 already reads the transition and throws when transition.snapshotPath !== snapshotPath. Lines 2500-2503 read the same file again and repeat the same check. The second read can observe a newer record, but both checks raise the identical error, so the extra read adds no guarantee.

Return the transition from the helper, or reuse the first read, so this function has one source of truth for the transition record.

🤖 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 2495 - 2506, Remove the duplicate
readShieldsDownTransition call and snapshot-path validation in the surrounding
auto-restore flow. Update inspectAutoRestoreTransitionTakeoverOwner to return
the transition it already reads, then reuse that returned record for the
subsequent waitForShieldsDownForwardCommit condition so there is one source of
truth.

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

Centralize the .containment path suffix in one exported helper.

This file builds the containment path from the string literal .containment at three places: Line 290, Line 881, and Line 1042. Line 1042 uses that path to decide whether to rethrow an error instead of retrying. If the suffix ever changes in mcp-lifecycle-lock, these sites keep compiling and silently stop detecting containment.

Export a getMcpLifecycleContainmentPath(sandboxName, stateDir) accessor from ../state/mcp-lifecycle-lock and call it here and at the other two sites.

♻️ Proposed change at this site
-  const containmentPath = `${getMcpLifecycleLockPath(sandboxName, STATE_DIR)}.containment`;
+  const containmentPath = getMcpLifecycleContainmentPath(sandboxName, STATE_DIR);
🤖 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 285 - 298, Export and use a shared
getMcpLifecycleContainmentPath(sandboxName, stateDir) helper from
../state/mcp-lifecycle-lock instead of constructing the ".containment" suffix
locally. Update persistUnresolvedShieldsContainment and the other two
containment-path call sites in this file, including the error-retry check, to
use the helper consistently.
src/lib/shields/timer-control.ts (1)

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

Remove the obsolete terminated field.

killTimer always returns terminated: false, and no production caller reads it. Remove it from KillTimerResult and update the assertions in src/lib/shields/index.test.ts.

🤖 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 236 - 253, Remove the obsolete
terminated property from the KillTimerResult type and the return object in
killTimer. Update the related assertions in shields/index.test.ts to stop
expecting terminated while preserving the remaining result fields and 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.

Inline comments:
In `@src/lib/shields/index.ts`:
- Around line 2979-2983: Update the ownerMcpProcessIdentity initialization in
the shields-down flow to use ownerStartIdentityFallback when
readMcpLockProcessIdentity(process.pid, true) returns null, rather than throwing
and blocking shutdown. Preserve the fresh lookup while allowing shields down to
proceed when /proc or ps cannot provide an identity.

In `@src/lib/shields/timer.ts`:
- Around line 477-505: Bound the retry loop in restoreWhileDeadlineOwned using
the same INTERACTIVE_AUTO_RESTORE_MAX_ATTEMPTS policy as the interactive
recovery path. Track attempts, stop retrying when the cap is reached, and
transition to the existing durable-containment terminal behavior while
preserving marker validation and normal complete/revoked outcomes.

In `@src/lib/state/mcp-lifecycle-lock-acquisition.test.ts`:
- Around line 75-88: Remove the if statement from
publishTimerWhenStaleOwnerIsObserved and use the file’s existing optional-call
idiom to conditionally publish the marker and throw ESRCH only when the PID is
2_147_483_647 and the signal is 0. Preserve delegation to realProcessKill for
all other process.kill calls.

---

Nitpick comments:
In `@docs/manage-sandboxes/backup-restore.mdx`:
- Around line 213-219: In the backup-all behavior description, update the newly
added references in the sentences around “backup-all” and “NemoClaw” to use
lowercase “shields” consistently, including “shields up,” “shields down,” and
“shields posture,” while preserving the existing meaning and wording.

In `@docs/reference/commands.mdx`:
- Around line 2913-2919: In the backup-all documentation, update both
occurrences of “Shields posture” in the affected lock and restore descriptions
to “shields posture,” preserving the existing wording and capitalization
elsewhere.

In `@src/lib/actions/maintenance.test.ts`:
- Around line 277-302: Update the test for backupAll so it captures the
console.error spy and asserts the logged failure includes the expected “backup
failed (mutation lock: ...)” reason from the withSandboxMutationLock rejection.
Keep the existing assertions verifying no container start or backup mutations
occur.
- Around line 833-877: Make the test’s loop-abort expectation explicit around
backupAll: verify the second sandbox, “beta,” is not processed after the cleanup
lock failure, using an observable public outcome rather than relying solely on
withSandboxMutationLock call counts. Keep the existing assertions for the
AggregateError and stopped-container cleanup behavior.
- Around line 277-302: Add a backupAll() test covering failure after setup
enters the acquired mutation lock: configure
startStoppedSandboxContainerForBackup to start a container, make
openBackupShieldsWindow throw, and make returnSandboxContainerToStopped return
false. Assert the operation rejects or exits with the AggregateError message
“Backup setup for '<name>' failed and its started container could not be
returned to the stopped state,” while verifying the started container is not
backed up and the failed restoration path is invoked.

In `@src/lib/actions/maintenance.ts`:
- Around line 378-420: Remove the redundant result, orphanManifestMessage,
mutationLockError, and mutationLockFailed locals around
backupSandboxWithinShieldsWindow. Read result and orphanManifestMessage directly
from attempt, and replace the mutationLockFailed branch with an inline
"mutationLockError" in attempt check while deriving the error detail from
attempt.mutationLockError.
- Around line 257-299: Replace the failure-combination cascade in the
maintenance failure-handling block with one causes collection using backupError
or orphanManifestMessage, relockError, and stoppedContainerCleanupError.
Aggregate when multiple causes exist and throw the sole cause unchanged when
only one exists. Preserve the exact currently asserted error messages by
deriving the message from the same combinations of present failure flags.

In `@src/lib/shields/index.ts`:
- Around line 937-987: Update retryInlineAutoRestore so that, immediately before
each Atomics.wait call, it writes one console.error line indicating inline
auto-restore is retrying behind the lifecycle gate. Keep the message in the
existing retry condition and preserve the current wait behavior and audit
logging.
- Around line 2495-2506: Remove the duplicate readShieldsDownTransition call and
snapshot-path validation in the surrounding auto-restore flow. Update
inspectAutoRestoreTransitionTakeoverOwner to return the transition it already
reads, then reuse that returned record for the subsequent
waitForShieldsDownForwardCommit condition so there is one source of truth.
- Around line 285-298: Export and use a shared
getMcpLifecycleContainmentPath(sandboxName, stateDir) helper from
../state/mcp-lifecycle-lock instead of constructing the ".containment" suffix
locally. Update persistUnresolvedShieldsContainment and the other two
containment-path call sites in this file, including the error-retry check, to
use the helper consistently.

In `@src/lib/shields/timer-bound-lock.ts`:
- Around line 85-97: Update withTimerBoundAutoRestoreLock to accept an optional
deps: TimerBoundLockDeps parameter defaulting to defaultDeps, and pass that
parameter to withTimerBoundShieldsMutationLockOptions instead of hardcoding
defaultDeps, matching the injectable dependency pattern of the related lock
functions.

In `@src/lib/shields/timer-control.ts`:
- Around line 236-253: Remove the obsolete terminated property from the
KillTimerResult type and the return object in killTimer. Update the related
assertions in shields/index.test.ts to stop expecting terminated while
preserving the remaining result fields and behavior.

In `@src/lib/shields/timer.test.ts`:
- Around line 92-100: Increase the timeout in waitForRetryBoundary’s vi.waitFor
options from 200 ms to approximately 2000 ms, while keeping the 1 ms polling
interval and file-existence assertions unchanged.

In `@src/lib/state/mcp-lifecycle-lock-acquisition.ts`:
- Around line 506-508: Update the main-lock-absent branch in the async
lifecycle-lock acquisition flow to clear corruptMainTracker.lastOwnerPid before
or alongside resetCorruptGenerationTracker. Match the equivalent behavior in
acquireMcpLifecycleLockSync while preserving the existing tracker reset logic.
- Around line 785-870: Update acquireDeadlineFence to require options:
McpLifecycleDeadlineFenceOptions & { stateDir: string }, matching
acquireDeadlineFenceSync and the guaranteed stateDir supplied by
withMcpLifecycleDeadlineFence. Remove the fallback resolveNemoclawStateDir()
from ensureDurableContainmentForStaleGenerationSync and use options.stateDir
directly so every state-directory read uses the required injected value.
- Around line 696-712: Document durableMcpLifecycleContainmentFailure with a doc
comment stating that it mutates the active lease by setting
retainForDurableContainment, preserving the lease across normal callback
completion; make clear that callers must not treat this error factory as pure.

In `@src/lib/state/mcp-lifecycle-lock-storage.ts`:
- Around line 152-156: Add a concise pointer comment to the synchronous
lifecycle-lock release/reclaim path, covering both
safelyReleaseMcpLifecycleLockSync and
reclaimStaleMcpLifecycleLockGenerationSync, that references the async
implementation’s atomic rename claim and hard-link restoration for raced
replacement owners. Do not change the synchronization logic.

In `@src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts`:
- Around line 91-121: Extract the repeated marker validation from
readShieldsTimerTakeoverToken and isShieldsTimerDeadlineExpired into a shared
helper that checks sandbox-name equality, string processToken type, and the
32-hex token format. Update both functions to reuse this predicate while
preserving their existing return behavior and deadline validation.
🪄 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: 2177445b-5b2e-4db0-bb78-53ba2f3ac67f

📥 Commits

Reviewing files that changed from the base of the PR and between c614c0d and 176e29e.

📒 Files selected for processing (32)
  • ci/env-var-doc-allowlist.json
  • ci/source-shape-test-budget.json
  • docs/manage-sandboxes/backup-restore.mdx
  • docs/manage-sandboxes/runtime-controls.mdx
  • docs/reference/commands.mdx
  • src/commands/sandbox/shields/status.ts
  • src/commands/sandbox/shields/up.ts
  • src/lib/actions/maintenance.test.ts
  • src/lib/actions/maintenance.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/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/helpers/isolate-test-state.ts
  • test/mcp-lifecycle-lock.test.ts
  • test/vitest-temp-root.test.ts
  • vitest.config.ts

Comment thread src/lib/shields/index.ts Outdated
Comment on lines +2979 to +2983
ownerMcpProcessIdentity:
readMcpLockProcessIdentity(process.pid, true) ??
(() => {
throw new Error("Cannot identify shields-down lifecycle owner process");
})(),

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect readMcpLockProcessIdentity to determine when it returns null.
set -euo pipefail

fd -t f 'mcp-lifecycle-lock-identity.ts' src

fd -t f 'mcp-lifecycle-lock-identity.ts' src --exec ast-grep outline {} --items all

# Show the full implementation and its null-return paths.
ast-grep run --lang typescript \
  --pattern 'export function readMcpLockProcessIdentity($$$) { $$$ }' \
  src

# Find every caller and how each handles a null result.
rg -nP -C4 '\breadMcpLockProcessIdentity\s*\(' src

Repository: NVIDIA/NemoClaw

Length of output: 1839


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=$(fd -t f 'mcp-lifecycle-lock-identity.ts' src | head -n1)
cat -n "$file" | sed -n '1,225p'

printf '\n--- call sites ---\n'
rg -n -C5 '\breadMcpLockProcessIdentity\s*\(' src

printf '\n--- transition-lock identity fallback ---\n'
rg -n -C8 'ownerStartIdentityFallback|readMcpLockProcessIdentity|processIdentity' src/lib/state/transition-lock.ts src/lib/state

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- fallback references ---'
rg -n -C6 'ownerStartIdentityFallback|transition-lock|start identity fallback' src

printf '%s\n' '--- supported platform declarations ---'
rg -n -i -C3 'windows|macos|darwin|linux|supported platform|platform support' README.md package.json src/lib/README.md docs 2>/dev/null || true

printf '%s\n' '--- shields-down identity context ---'
cat -n src/lib/shields/index.ts | sed -n '2968,2990p'

Repository: NVIDIA/NemoClaw

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact fallback locations ---'
rg -l 'ownerStartIdentityFallback' src | while read -r file; do
  printf '\nFILE %s\n' "$file"
  rg -n -C12 'ownerStartIdentityFallback' "$file"
done

printf '%s\n' '--- exact platform support references ---'
rg -n -i 'Windows|macOS|Linux' docs/reference/platform-support.mdx docs/get-started/prerequisites.mdx docs/get-started/additional-setup/windows-preparation.mdx 2>/dev/null | head -n 120 || true

Repository: NVIDIA/NemoClaw

Length of output: 14116


Use the identity fallback for shields down. fresh = true only bypasses the cache. It does not prevent readMcpLockProcessIdentity from returning null when /proc or ps cannot provide an identity. Reuse ownerStartIdentityFallback or handle null without blocking shields down.

🤖 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 2979 - 2983, Update the
ownerMcpProcessIdentity initialization in the shields-down flow to use
ownerStartIdentityFallback when readMcpLockProcessIdentity(process.pid, true)
returns null, rather than throwing and blocking shutdown. Preserve the fresh
lookup while allowing shields down to proceed when /proc or ps cannot provide an
identity.

Comment thread src/lib/shields/timer.ts
Comment thread src/lib/state/mcp-lifecycle-lock-acquisition.test.ts
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Informational

Advisor assessment: Informational / low confidence
Next action: No advisor follow-up needed.
Findings: 0 blockers · 0 warnings · 0 suggestions
Status: Partial review preserved 0 canonical finding(s) and 3 terminology decision(s) before the advisor stopped.

Model lanes

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

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.

  • define — lifecycle deadline gate at docs/manage-sandboxes/runtime-controls.mdx:111: Define this term at first use as the per-sandbox gate that blocks new mutations after the Shields deadline.
  • define — durable containment at docs/manage-sandboxes/runtime-controls.mdx:122: Define this term at first use as persisted fail-closed containment that blocks mutations pending operator recovery.
  • define — exact-generation recovery at docs/manage-sandboxes/backup-restore.mdx:97: Define this term at first use as recovery that acts only on the recorded lock, deadline, and containment generations.

E2E guidance

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

Recommended E2E: cloud-inference, cloud-onboard, security-posture, 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.

@jyaunches

Copy link
Copy Markdown
Contributor Author

Stacked focused follow-up: #8141 contains the managed MCP policy reconciliation and corrected Hermes regression order. Its standard CI, automated reviews, and all 10 trusted E2E selections are green.

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

Blocking finding confirmed at exact head c023a4b: restoreWhileDeadlineOwned in src/lib/shields/timer.ts retries forever when restore or re-lock keeps failing and the timer marker remains current. The callback runs inside withMcpLifecycleDeadlineFence, which releases its main lock and deadline fence only after the callback returns. A deterministic persistent failure therefore leaves ordinary lifecycle, policy, channel, shields, and snapshot operations blocked indefinitely while audit entries continue accumulating. This makes supported recovery workflows unusable. Please bound the timer retry loop by attempts or elapsed time, then enter the existing durable-containment terminal state with actionable operator recovery while retaining the fail-closed lifecycle gate. The existing CodeRabbit inline identifies the same affected loop, so I am not duplicating an inline comment.

<!-- 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>

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

🧹 Nitpick comments (5)
src/lib/shields/index.ts (1)

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

Extract the recovery-token validation into one helper.

The literal /^[0-9a-f]{32}$/ test on marker.processToken appears three times in this function, and it also appears at Line 2728 and Line 3728. One helper keeps the token contract in a single place and prevents drift if the token format changes.

♻️ Suggested helper
function isRecoveryProcessToken(value: string | undefined): value is string {
  return typeof value === "string" && /^[0-9a-f]{32}$/.test(value);
}
-  if (marker.processToken && /^[0-9a-f]{32}$/.test(marker.processToken)) {
+  if (isRecoveryProcessToken(marker.processToken)) {

Also applies to: 3042-3048, 3078-3078

🤖 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` at line 3014, Extract the repeated recovery-token
regex into a shared isRecoveryProcessToken helper near the relevant utilities,
accepting string | undefined and narrowing valid values to string. Replace the
direct /^[0-9a-f]{32}$/ checks in the current function and the other occurrences
around the referenced token-validation logic with this helper, preserving the
existing validation behavior.
src/lib/shields/flow.test.ts (1)

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

Derive the fixture ownership values from the shared constants.

managedMcpSandbox hardcodes sourcePath: "generated:nemoclaw-mcp-bridge" and policyName: \mcp-bridge-${server}`. Production ownership checks compare against MCP_BRIDGE_POLICY_SOURCEandbuildMcpBridgePolicyName(server)`. If either value changes, this fixture stops representing an owned bridge and the managed MCP tests lose their meaning.

Import both symbols instead.

♻️ Proposed fixture change
-import { buildMcpBridgePolicyYaml } from "../actions/sandbox/mcp-bridge-policy-render";
+import { MCP_BRIDGE_POLICY_SOURCE } from "../actions/sandbox/mcp-bridge-policy";
+import {
+  buildMcpBridgePolicyName,
+  buildMcpBridgePolicyYaml,
+} from "../actions/sandbox/mcp-bridge-policy-render";
     customPolicies: policies.map(({ content, server }) => ({
-      name: `mcp-bridge-${server}`,
+      name: buildMcpBridgePolicyName(server),
       content,
-      sourcePath: "generated:nemoclaw-mcp-bridge",
+      sourcePath: MCP_BRIDGE_POLICY_SOURCE,
     })),
-            policyName: `mcp-bridge-${server}`,
+            policyName: buildMcpBridgePolicyName(server),
🤖 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 80 - 106, Update the
managedMcpSandbox fixture to import and use the shared MCP_BRIDGE_POLICY_SOURCE
and buildMcpBridgePolicyName symbols for sourcePath and policyName,
respectively, instead of hardcoded ownership values. Keep the generated fixture
behavior unchanged while ensuring it follows production ownership checks.
src/lib/shields/timer.test.ts (1)

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

Type the mock result with the exported omission type.

The inline type makes server required. ManagedMcpPolicyOmission in src/lib/actions/sandbox/mcp-bridge-policy.ts (lines 40-45) declares key, policyName, and server as optional and only reason as required. Key-only omissions, such as the reserved-key case, cannot be expressed by this mock. The mock then no longer tracks the real contract if timer.ts starts reading key or policyName.

♻️ Proposed mock typing
+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 }),
   ),

Note that vi.hoisted runs before imports, so keep the reference type-only.

🤖 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 - 21, Update the
applyShieldsPolicySnapshot mock result in shieldsIndexMock to use the exported
ManagedMcpPolicyOmission type from mcp-bridge-policy.ts via a type-only
reference, preserving vi.hoisted’s runtime import constraints. Remove the inline
omission shape so optional key, policyName, and server fields and required
reason match the production contract.
src/lib/shields/permissive-runtime.ts (1)

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

Extract the shared temp-policy staging logic and wrap the base read.

buildDeadlineRuntimeManagedMcpPolicy repeats the staging and cleanup logic of buildRuntimeManagedMcpPolicy (lines 214-234) with a different branch shape. Both paths must keep identical cleanup semantics for the 0700 mkdtemp directory, so one helper is safer than two copies.

Line 246 also calls deps.readBasePolicy() without the contextual wrapper used at lines 202-208. A snapshot read failure at the deadline then surfaces a raw filesystem error to the operator instead of the staging message.

♻️ Proposed shared helper
+function stageRuntimePolicy(
+  yaml: string,
+  deps: ManagedMcpRuntimePolicyDeps,
+  failureMessage: string,
+): string {
+  if (deps.writeTempPolicy) {
+    try {
+      return deps.writeTempPolicy(yaml);
+    } catch (error) {
+      throw new Error(failureMessage, { cause: error });
+    }
+  }
+  let tmpPath: string | null = null;
+  try {
+    tmpPath = secureTempFile(TEMP_FILE_PREFIX, ".yaml");
+    fs.writeFileSync(tmpPath, yaml, { mode: 0o600 });
+    return tmpPath;
+  } catch (error) {
+    if (tmpPath) cleanupTempDir(tmpPath, TEMP_FILE_PREFIX);
+    throw new Error(failureMessage, { cause: error });
+  }
+}
 ): DeadlineManagedMcpRuntimePolicy {
-  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,
+    });
+  }
🤖 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 242 - 269, Extract the
duplicated temporary-policy writing and cleanup flow from
buildRuntimeManagedMcpPolicy and buildDeadlineRuntimeManagedMcpPolicy into one
shared helper, preserving identical 0700 temp-directory cleanup semantics and
error propagation. Update both builders to use that helper, and wrap
deps.readBasePolicy() in buildDeadlineRuntimeManagedMcpPolicy with the same
contextual error message used by the existing base-policy read path.
src/lib/actions/sandbox/mcp-bridge-policy.ts (1)

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

Consider sharing the policy-document helpers with src/lib/shields/mcp-policy-transition.ts.

parseManagedPolicyDocument and readManagedNetworkPolicies duplicate parsePolicyDocument and readNetworkPolicies in src/lib/shields/mcp-policy-transition.ts (lines 14-37). The two copies can drift on parse strictness, and both feed the same fail-closed decisions. Extract one shared helper module.

🤖 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/actions/sandbox/mcp-bridge-policy.ts` around lines 60 - 83, Extract
the shared YAML policy parsing and network-policy mapping logic from
parseManagedPolicyDocument and readManagedNetworkPolicies into a common helper
module, then update both mcp-bridge-policy.ts and mcp-policy-transition.ts to
reuse it. Preserve the existing validation, error behavior, and fail-closed
handling in both callers while removing the duplicated implementations.
🤖 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`:
- Line 119: Update the sentence in the runtime controls documentation to
hyphenate the compound modifier, changing “snapshot-time managed MCP entries” to
“snapshot-time-managed MCP entries.”

In `@scripts/checks/openshell-policy-mutation-read.mts`:
- Line 61: Replace the count-only allowance around expectedReadCalls and
buildPolicyGetCommand with a canonical, site-specific invariant that identifies
and validates the three approved policy-read call sites. Update the audit to
reject any unapproved extra read even when the count is adjusted, preserve the
intended three-read behavior, and add focused tests covering both rejection of
an extra read and acceptance of the approved sites.

In `@test/e2e/live/mcp-bridge-sandbox.ts`:
- Around line 80-86: Update expectExitNonZero to explicitly reject null exitCode
values before the output-pattern assertion, while still requiring a completed
nonzero exit status. Preserve the existing diagnostic message and resultText
matching for valid nonzero exits.

In `@test/e2e/support/mcp-bridge-sandbox.test.ts`:
- Around line 306-333: Update the test around
assertManagedMcpPolicySurvivedRemoval to use independent, structurally
equivalent policy snapshots rather than reusing the same survivingPolicy object.
Add a preservation assertion with the removed policy key absent and a separately
constructed surviving-policy object, while retaining the failing case where that
key remains present, so the test verifies structural preservation rather than
object identity.

---

Nitpick comments:
In `@src/lib/actions/sandbox/mcp-bridge-policy.ts`:
- Around line 60-83: Extract the shared YAML policy parsing and network-policy
mapping logic from parseManagedPolicyDocument and readManagedNetworkPolicies
into a common helper module, then update both mcp-bridge-policy.ts and
mcp-policy-transition.ts to reuse it. Preserve the existing validation, error
behavior, and fail-closed handling in both callers while removing the duplicated
implementations.

In `@src/lib/shields/flow.test.ts`:
- Around line 80-106: Update the managedMcpSandbox fixture to import and use the
shared MCP_BRIDGE_POLICY_SOURCE and buildMcpBridgePolicyName symbols for
sourcePath and policyName, respectively, instead of hardcoded ownership values.
Keep the generated fixture behavior unchanged while ensuring it follows
production ownership checks.

In `@src/lib/shields/index.ts`:
- Line 3014: Extract the repeated recovery-token regex into a shared
isRecoveryProcessToken helper near the relevant utilities, accepting string |
undefined and narrowing valid values to string. Replace the direct
/^[0-9a-f]{32}$/ checks in the current function and the other occurrences around
the referenced token-validation logic with this helper, preserving the existing
validation behavior.

In `@src/lib/shields/permissive-runtime.ts`:
- Around line 242-269: Extract the duplicated temporary-policy writing and
cleanup flow from buildRuntimeManagedMcpPolicy and
buildDeadlineRuntimeManagedMcpPolicy into one shared helper, preserving
identical 0700 temp-directory cleanup semantics and error propagation. Update
both builders to use that helper, and wrap deps.readBasePolicy() in
buildDeadlineRuntimeManagedMcpPolicy with the same contextual error message used
by the existing base-policy read path.

In `@src/lib/shields/timer.test.ts`:
- Around line 11-21: Update the applyShieldsPolicySnapshot mock result in
shieldsIndexMock to use the exported ManagedMcpPolicyOmission type from
mcp-bridge-policy.ts via a type-only reference, preserving vi.hoisted’s runtime
import constraints. Remove the inline omission shape so optional key,
policyName, and server fields and required reason match the production 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: 0421881b-d059-4033-b09a-2d0dc5d6f47f

📥 Commits

Reviewing files that changed from the base of the PR and between c023a4b and 0f6d801.

📒 Files selected for processing (19)
  • 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/permissive-runtime.test.ts
💤 Files with no reviewable changes (1)
  • test/e2e/live/mcp-bridge-hermes-lifecycle.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/reference/commands.mdx
  • src/lib/shields/timer.ts

Retry a command that the auto-restore deadline interrupts after you open a new shields-down window.
Before a manual Shields transition replaces a policy, NemoClaw requires exact Model Context Protocol (MCP) agreement among the sandbox registry, generated-policy record, and live gateway policy.
`shields down` carries the proven managed MCP policy entries into the relaxed policy.
Restoration removes snapshot-time managed MCP entries before it overlays current exact entries.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hyphenate the compound modifier.

Change snapshot-time managed MCP entries to snapshot-time-managed MCP entries.

🧰 Tools
🪛 LanguageTool

[grammar] ~119-~119: Use a hyphen to join words.
Context: ...olicy. Restoration removes snapshot-time managed MCP entries before it overlays c...

(QB_NEW_EN_HYPHEN)

🤖 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/manage-sandboxes/runtime-controls.mdx` at line 119, Update the sentence
in the runtime controls documentation to hyphenate the compound modifier,
changing “snapshot-time managed MCP entries” to “snapshot-time-managed MCP
entries.”

Source: Linters/SAST tools

Comment thread scripts/checks/openshell-policy-mutation-read.mts Outdated
Comment thread test/e2e/live/mcp-bridge-sandbox.ts Outdated
Comment on lines +80 to +86
export function expectExitNonZero(result: ShellProbeResult, label: string, pattern: RegExp): void {
assert.notEqual(
result.exitCode,
0,
`${label}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
assert.match(resultText(result), pattern);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a completed nonzero exit.

ShellProbeResult.exitCode can be null. Line 81 accepts that value as a valid failure. A timed-out or signaled probe can then satisfy the output assertion without completing the expected rejection path. Reject timed-out probes and null exit codes before matching output.

Proposed fix
 export function expectExitNonZero(result: ShellProbeResult, label: string, pattern: RegExp): void {
-  assert.notEqual(
-    result.exitCode,
-    0,
+  assert.ok(
+    !result.timedOut && result.exitCode !== null && result.exitCode !== 0,
     `${label}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
   );
   assert.match(resultText(result), pattern);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function expectExitNonZero(result: ShellProbeResult, label: string, pattern: RegExp): void {
assert.notEqual(
result.exitCode,
0,
`${label}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
assert.match(resultText(result), pattern);
export function expectExitNonZero(result: ShellProbeResult, label: string, pattern: RegExp): void {
assert.ok(
!result.timedOut && result.exitCode !== null && result.exitCode !== 0,
`${label}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
assert.match(resultText(result), pattern);
}
🤖 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/live/mcp-bridge-sandbox.ts` around lines 80 - 86, Update
expectExitNonZero to explicitly reject null exitCode values before the
output-pattern assertion, while still requiring a completed nonzero exit status.
Preserve the existing diagnostic message and resultText matching for valid
nonzero exits.

Comment on lines +306 to +333
it("accepts an unchanged surviving policy only after the unrelated policy is absent", () => {
const survivingPolicy = {
endpoints: [{ host: "surviving.example.test", allowed_ips: ["203.0.113.10"] }],
};

it("captures the Hermes rediscovery offset after route removal and before restart", () => {
const source = fs.readFileSync("test/e2e/live/mcp-bridge.test.ts", "utf8");
const denialProof = source.indexOf("rebound request must not reach the upstream MCP server");
const restore = source.indexOf("await restoreDnsRebindingHostsFixture", denialProof);
const remove = source.indexOf("const remove = await host.nemoclaw", denialProof);
const hermesTest = source.indexOf('mcpBridgeShardTest("hermes")');
const rebinding = source.indexOf("await assertAdapterDnsRebindingDenied", hermesTest);
const offset = source.indexOf(
"const survivingDiscoveryOffset = fakeMcp.requests.length",
rebinding,
);
const restart = source.indexOf("await restartBridgeWithoutHostSecret", offset);
const toolCall = source.indexOf("await assertRealAdapterToolCall", restart);
const rediscovery = source.indexOf("await assertAuthenticatedMcpRediscovery", toolCall);

expect(denialProof).toBeGreaterThanOrEqual(0);
expect(restore).toBeGreaterThan(denialProof);
expect(remove).toBeGreaterThan(restore);
expect(rebinding).toBeGreaterThan(hermesTest);
expect(offset).toBeGreaterThan(rebinding);
expect(restart).toBeGreaterThan(offset);
expect(toolCall).toBeGreaterThan(restart);
expect(rediscovery).toBeGreaterThan(toolCall);
expect(source).toContain("Hermes MCP rediscovery after explicit restart");
expect(() =>
assertManagedMcpPolicySurvivedRemoval(
survivingPolicy,
{
networkPolicies: { mcp_bridge_surviving: survivingPolicy },
policy: survivingPolicy,
},
"mcp_bridge_rebinding",
),
).not.toThrow();
expect(() =>
assertManagedMcpPolicySurvivedRemoval(
survivingPolicy,
{
networkPolicies: {
mcp_bridge_rebinding: { endpoints: [] },
mcp_bridge_surviving: survivingPolicy,
},
policy: survivingPolicy,
},
"mcp_bridge_rebinding",
),
).toThrow();

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use independent policy snapshots in the preservation case.

Lines 313-317 use the same survivingPolicy object for both snapshots. This test would pass if the helper used identity comparison instead of structural comparison. The failing case only proves that the removed policy key must be absent. Add a separate changed surviving-policy case with the removed key absent.

Proposed fix
-    const survivingPolicy = {
+    const survivingPolicyBefore = {
       endpoints: [{ host: "surviving.example.test", allowed_ips: ["203.0.113.10"] }],
     };
+    const survivingPolicyAfter = {
+      endpoints: [{ host: "surviving.example.test", allowed_ips: ["203.0.113.10"] }],
+    };
+    const changedSurvivingPolicy = {
+      endpoints: [{ host: "changed.example.test", allowed_ips: ["203.0.113.10"] }],
+    };

     expect(() =>
       assertManagedMcpPolicySurvivedRemoval(
-        survivingPolicy,
+        survivingPolicyBefore,
         {
-          networkPolicies: { mcp_bridge_surviving: survivingPolicy },
-          policy: survivingPolicy,
+          networkPolicies: { mcp_bridge_surviving: survivingPolicyAfter },
+          policy: survivingPolicyAfter,
         },
         "mcp_bridge_rebinding",
       ),
     ).not.toThrow();
+
+    expect(() =>
+      assertManagedMcpPolicySurvivedRemoval(
+        survivingPolicyBefore,
+        {
+          networkPolicies: { mcp_bridge_surviving: changedSurvivingPolicy },
+          policy: changedSurvivingPolicy,
+        },
+        "mcp_bridge_rebinding",
+      ),
+    ).toThrow();

As per path instructions, review tests for behavioral confidence rather than implementation lock-in.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("accepts an unchanged surviving policy only after the unrelated policy is absent", () => {
const survivingPolicy = {
endpoints: [{ host: "surviving.example.test", allowed_ips: ["203.0.113.10"] }],
};
it("captures the Hermes rediscovery offset after route removal and before restart", () => {
const source = fs.readFileSync("test/e2e/live/mcp-bridge.test.ts", "utf8");
const denialProof = source.indexOf("rebound request must not reach the upstream MCP server");
const restore = source.indexOf("await restoreDnsRebindingHostsFixture", denialProof);
const remove = source.indexOf("const remove = await host.nemoclaw", denialProof);
const hermesTest = source.indexOf('mcpBridgeShardTest("hermes")');
const rebinding = source.indexOf("await assertAdapterDnsRebindingDenied", hermesTest);
const offset = source.indexOf(
"const survivingDiscoveryOffset = fakeMcp.requests.length",
rebinding,
);
const restart = source.indexOf("await restartBridgeWithoutHostSecret", offset);
const toolCall = source.indexOf("await assertRealAdapterToolCall", restart);
const rediscovery = source.indexOf("await assertAuthenticatedMcpRediscovery", toolCall);
expect(denialProof).toBeGreaterThanOrEqual(0);
expect(restore).toBeGreaterThan(denialProof);
expect(remove).toBeGreaterThan(restore);
expect(rebinding).toBeGreaterThan(hermesTest);
expect(offset).toBeGreaterThan(rebinding);
expect(restart).toBeGreaterThan(offset);
expect(toolCall).toBeGreaterThan(restart);
expect(rediscovery).toBeGreaterThan(toolCall);
expect(source).toContain("Hermes MCP rediscovery after explicit restart");
expect(() =>
assertManagedMcpPolicySurvivedRemoval(
survivingPolicy,
{
networkPolicies: { mcp_bridge_surviving: survivingPolicy },
policy: survivingPolicy,
},
"mcp_bridge_rebinding",
),
).not.toThrow();
expect(() =>
assertManagedMcpPolicySurvivedRemoval(
survivingPolicy,
{
networkPolicies: {
mcp_bridge_rebinding: { endpoints: [] },
mcp_bridge_surviving: survivingPolicy,
},
policy: survivingPolicy,
},
"mcp_bridge_rebinding",
),
).toThrow();
it("accepts an unchanged surviving policy only after the unrelated policy is absent", () => {
const survivingPolicyBefore = {
endpoints: [{ host: "surviving.example.test", allowed_ips: ["203.0.113.10"] }],
};
const survivingPolicyAfter = {
endpoints: [{ host: "surviving.example.test", allowed_ips: ["203.0.113.10"] }],
};
const changedSurvivingPolicy = {
endpoints: [{ host: "changed.example.test", allowed_ips: ["203.0.113.10"] }],
};
expect(() =>
assertManagedMcpPolicySurvivedRemoval(
survivingPolicyBefore,
{
networkPolicies: { mcp_bridge_surviving: survivingPolicyAfter },
policy: survivingPolicyAfter,
},
"mcp_bridge_rebinding",
),
).not.toThrow();
expect(() =>
assertManagedMcpPolicySurvivedRemoval(
survivingPolicyBefore,
{
networkPolicies: { mcp_bridge_surviving: changedSurvivingPolicy },
policy: changedSurvivingPolicy,
},
"mcp_bridge_rebinding",
),
).toThrow();
🤖 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 306 - 333, Update
the test around assertManagedMcpPolicySurvivedRemoval to use independent,
structurally equivalent policy snapshots rather than reusing the same
survivingPolicy object. Add a preservation assertion with the removed policy key
absent and a separately constructed surviving-policy object, while retaining the
failing case where that key remains present, so the test verifies structural
preservation rather than object identity.

Source: Path instructions

@copy-pr-bot

copy-pr-bot Bot commented Aug 4, 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 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

Caution

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

⚠️ Outside diff range comments (1)
src/lib/shields/index.ts (1)

3213-3226: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the argument order in activateLockdownFromSnapshot.

Pass undefined for allowLegacyHermesProtocol, then cachedTarget, undefined for cachedProtocol, and the recovery options as the sixth argument. The current call fails type-checking and does not pass the recovery options to applyShieldsPolicySnapshot.

🤖 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 3213 - 3226, Correct the argument
order in the activateLockdownFromSnapshot call: pass undefined for
allowLegacyHermesProtocol, cachedTarget as the next argument, undefined for
cachedProtocol, and the existing recovery options as the sixth argument so they
reach applyShieldsPolicySnapshot.

Source: Linters/SAST tools

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

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

Replace the positional fork argument assertion.

timerArgs.at(9) couples this test to the internal timer argv layout. A harmless argument reorder will fail the test.

Assert the observable timer behavior instead. Use the persisted marker or the recovery outcome. 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/flow.test.ts` at line 1014, Replace the positional timerArgs
assertion in the relevant flow test with an assertion on observable behavior
through the public boundary, using the persisted marker or recovery outcome.
Remove dependence on the internal timer argv layout while preserving coverage
that the timer flow handles the “openclaw” fork behavior correctly.

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/shields/timer.test.ts`:
- Around line 471-473: Update the audit-ordering assertion in
invokeTimerAndCaptureExit’s test to avoid requiring distinct millisecond
timestamps; verify the failed audit precedes the success audit using the audit
collection’s write/order information, or otherwise use a non-strict comparison
that preserves the intended ordering.

In `@src/lib/state/mcp-lifecycle-lock/shields-timer-authority.ts`:
- Around line 70-79: Update readShieldsTimerMarker to validate that the parsed
marker’s sandboxName matches the requested sandboxName before returning it;
return null for mismatches so recovery cannot use another sandbox’s snapshot.
Add a test covering a marker file for one sandbox being rejected when read for a
different sandbox.

---

Outside diff comments:
In `@src/lib/shields/index.ts`:
- Around line 3213-3226: Correct the argument order in the
activateLockdownFromSnapshot call: pass undefined for allowLegacyHermesProtocol,
cachedTarget as the next argument, undefined for cachedProtocol, and the
existing recovery options as the sixth argument so they reach
applyShieldsPolicySnapshot.

---

Nitpick comments:
In `@src/lib/shields/flow.test.ts`:
- Line 1014: Replace the positional timerArgs assertion in the relevant flow
test with an assertion on observable behavior through the public boundary, using
the persisted marker or recovery outcome. Remove dependence on the internal
timer argv layout while preserving coverage that the timer flow handles the
“openclaw” fork behavior correctly.
🪄 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: d10eeb70-49b2-4c1b-ac14-6de3c81e7619

📥 Commits

Reviewing files that changed from the base of the PR and between 0f6d801 and ca34650.

📒 Files selected for processing (11)
  • ci/source-architecture-budget.json
  • ci/source-shape-test-budget.json
  • docs/reference/commands.mdx
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.ts
  • src/lib/shields/policy-transition.test.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/shields-timer-authority.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • ci/source-shape-test-budget.json
  • src/lib/shields/transition-lock.ts
  • ci/source-architecture-budget.json
  • docs/reference/commands.mdx
  • src/lib/shields/timer.ts

Comment thread src/lib/shields/timer.test.ts Outdated
Comment on lines +471 to +473
expect(Date.parse(successAudits[0].timestamp)).toBeGreaterThan(
Date.parse(failedAudit.timestamp),
);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the audit-ordering assertion resistant to same-millisecond timestamps.

invokeTimerAndCaptureExit passes retryDelayMs: 1. The failed audit and the success audit can therefore both land in the same millisecond. ISO-8601 timestamps have millisecond resolution, so Date.parse returns equal values and toBeGreaterThan fails intermittently.

Assert the relative order instead, or relax the comparison.

🩹 Proposed fix using write order
-    expect(Date.parse(successAudits[0].timestamp)).toBeGreaterThan(
-      Date.parse(failedAudit.timestamp),
-    );
+    expect(audits.indexOf(successAudits[0])).toBeGreaterThan(audits.indexOf(failedAudit));
+    expect(Date.parse(successAudits[0].timestamp)).toBeGreaterThanOrEqual(
+      Date.parse(failedAudit.timestamp),
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(Date.parse(successAudits[0].timestamp)).toBeGreaterThan(
Date.parse(failedAudit.timestamp),
);
expect(audits.indexOf(successAudits[0])).toBeGreaterThan(
audits.indexOf(failedAudit),
);
expect(Date.parse(successAudits[0].timestamp)).toBeGreaterThanOrEqual(
Date.parse(failedAudit.timestamp),
);
🤖 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 471 - 473, Update the
audit-ordering assertion in invokeTimerAndCaptureExit’s test to avoid requiring
distinct millisecond timestamps; verify the failed audit precedes the success
audit using the audit collection’s write/order information, or otherwise use a
non-strict comparison that preserves the intended ordering.

Comment on lines +70 to +79
export function readShieldsTimerMarker(
sandboxName: string,
stateDir = resolveNemoclawStateDir(),
): ShieldsTimerMarker | null {
try {
return readShieldsTimerMarkerFile(shieldsTimerMarkerPath(sandboxName, stateDir));
} catch {
return null;
}
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject a timer marker for another sandbox.

readShieldsTimerMarker returns a valid marker even when marker.sandboxName !== sandboxName. The expired inline recovery path then consumes marker.snapshotPath without an equivalent check.

If shields-timer-alpha.json contains a marker for beta, recovery for alpha can apply beta's snapshot. Return null unless the parsed marker sandbox name matches the requested sandbox. Add a mismatched-marker test.

🤖 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/shields-timer-authority.ts` around lines 70
- 79, Update readShieldsTimerMarker to validate that the parsed marker’s
sandboxName matches the requested sandboxName before returning it; return null
for mismatches so recovery cannot use another sandbox’s snapshot. Add a test
covering a marker file for one sandbox being rejected when read for a different
sandbox.

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

Re-reviewed exact head ca34650. The blocking availability defect remains in src/lib/shields/timer.ts: restoreWhileDeadlineOwned still uses an unbounded for loop, and every persistent policy-restore or config re-lock failure returns retry while the marker remains valid. Because this loop runs inside withMcpLifecycleDeadlineFence, it can retain the sandbox lifecycle deadline indefinitely, make ordinary supported mutations time out, and append audit records without bound. Please cap attempts or wall-clock duration using the existing interactive recovery policy, then enter the established durable-containment terminal path and release the fence. Add a regression with a persistently failing restore that proves the timer terminates, the deadline file is released, and a waiting lifecycle mutation proceeds. No other current automated suggestions are blocking this review.

Stop detached restore retries after seven attempts.

Preserve exact lifecycle authority through durable containment or retained gates.

Sanitize MCP policy diagnostics and add race-focused regression coverage.

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Count deadline setup, publication, and restoration against one bounded retry budget.

On exhaustion, commit durable containment.

If that commit fails, retain exact owned gates and return actionable recovery guidance.

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

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

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

Build the injected failure with the production helper.

This block hand-writes the durable-containment error shape, including the NEMOCLAW_DURABLE_CONTAINMENT code string and the retainOwnedLifecycleGates flag. The test then asserts the same contract at Line 1415. If production renames the code or changes the flag, this test keeps passing while the real containment path breaks.

Call durableMcpLifecycleContainmentFailure from src/lib/state/mcp-lifecycle-lock-acquisition.ts with { retainOwnedLifecycleGates: true } instead.

As per path instructions: "Flag copied production algorithms, broad mocks that bypass the behavior under test".

🤖 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 1392 - 1400, Replace the
hand-built error in the beginContainment mock with a call to the production
helper durableMcpLifecycleContainmentFailure, passing {
retainOwnedLifecycleGates: true }. Keep the injected failure behavior and
existing assertions unchanged while ensuring the test uses the production error
contract.

Source: Path instructions

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

138-151: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the call count unconditionally.

policyApplicationsBeforeRevocation stays undefined when waitForRetryBoundary throws, so the if at Line 146 skips the assertion. The outer failure still propagates, so no defect hides here, but the guard adds a conditional that the test does not need. Capture the count before the try, or assert inside the try right after the capture. The test body then stays linear, which also matches the growth guardrail for test conditionals.

♻️ Proposed restructure
     const pending = runRestoreTimer(args, { retryDelayMs: 50 });
-    let policyApplicationsBeforeRevocation: number | undefined;
     try {
       await waitForRetryBoundary(deadlinePath, auditPath);
       expect(exitSpy).not.toHaveBeenCalled();
       expect(fs.existsSync(deadlinePath)).toBe(true);
-      policyApplicationsBeforeRevocation =
-        shieldsIndexMock.applyShieldsPolicySnapshot.mock.calls.length;
+      const policyApplicationsBeforeRevocation =
+        shieldsIndexMock.applyShieldsPolicySnapshot.mock.calls.length;
+      fs.rmSync(markerPath, { force: true });
+      await pending;
+      expect(shieldsIndexMock.applyShieldsPolicySnapshot).toHaveBeenCalledTimes(
+        policyApplicationsBeforeRevocation,
+      );
     } finally {
-      fs.rmSync(markerPath, { force: true });
-      await pending;
       fs.writeFileSync(markerPath, markerContents);
       exitSpy.mockRestore();
     }
-    if (policyApplicationsBeforeRevocation !== undefined) {
-      expect(shieldsIndexMock.applyShieldsPolicySnapshot).toHaveBeenCalledTimes(
-        policyApplicationsBeforeRevocation,
-      );
-    }
   }

As per path instructions: "conditionals that make a test pass without exercising its claim".

🤖 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 138 - 151, Make the call-count
assertion unconditional in the test around the
policyApplicationsBeforeRevocation capture. Move the capture before the
try/finally or assert immediately after capturing it inside the try, then remove
the if guard while preserving the cleanup and pending-retry handling.

Source: Path instructions

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

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

Use committedContainmentPath for all containment-path checks.

Export the state helper and use it at lines 312, 973, and 1136. This keeps the security-relevant .containment path definition 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/index.ts` around lines 312 - 313, Update the containment
checks in the relevant lifecycle logic to use the exported state helper’s
committedContainmentPath value instead of constructing paths directly with
getMcpLifecycleLockPath and the .containment suffix. Apply this consistently at
the checks near the current location and the corresponding locations around
lines 973 and 1136, preserving their existing control flow.
🤖 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-recovery-budget.test.ts`:
- Around line 119-127: Remove the path-based conditional fault-injection logic
from the mockImplementation blocks in the timer-recovery budget tests. Add or
reuse a shared helper such as failFsCallForPath in test/helpers to encapsulate
matching the target path, tracking failure attempts, creating the configured
filesystem error, and delegating other calls; update the affected mkdir and
filesystem mock call sites to use that helper as single expressions while
preserving existing failure behavior.

---

Nitpick comments:
In `@src/lib/shields/flow.test.ts`:
- Around line 1392-1400: Replace the hand-built error in the beginContainment
mock with a call to the production helper durableMcpLifecycleContainmentFailure,
passing { retainOwnedLifecycleGates: true }. Keep the injected failure behavior
and existing assertions unchanged while ensuring the test uses the production
error contract.

In `@src/lib/shields/index.ts`:
- Around line 312-313: Update the containment checks in the relevant lifecycle
logic to use the exported state helper’s committedContainmentPath value instead
of constructing paths directly with getMcpLifecycleLockPath and the .containment
suffix. Apply this consistently at the checks near the current location and the
corresponding locations around lines 973 and 1136, preserving their existing
control flow.

In `@src/lib/shields/timer.test.ts`:
- Around line 138-151: Make the call-count assertion unconditional in the test
around the policyApplicationsBeforeRevocation capture. Move the capture before
the try/finally or assert immediately after capturing it inside the try, then
remove the if guard while preserving the cleanup and pending-retry handling.
🪄 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: 55d392ce-dd59-4a84-8c4b-bffdc23ad2b9

📥 Commits

Reviewing files that changed from the base of the PR and between ca34650 and f0f888b.

📒 Files selected for processing (13)
  • docs/manage-sandboxes/backup-restore.mdx
  • docs/manage-sandboxes/runtime-controls.mdx
  • docs/reference/commands.mdx
  • src/lib/actions/sandbox/mcp-bridge-policy.ts
  • src/lib/actions/sandbox/snapshot-baseline-exclusion-output.test.ts
  • src/lib/shields/flow.test.ts
  • src/lib/shields/index.ts
  • src/lib/shields/mcp-policy-transition.test.ts
  • src/lib/shields/timer-recovery-budget.test.ts
  • src/lib/shields/timer.test.ts
  • src/lib/shields/timer.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.test.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • docs/manage-sandboxes/backup-restore.mdx
  • docs/manage-sandboxes/runtime-controls.mdx
  • src/lib/shields/mcp-policy-transition.test.ts
  • src/lib/state/mcp-lifecycle-lock-acquisition.test.ts
  • src/lib/actions/sandbox/mcp-bridge-policy.ts
  • docs/reference/commands.mdx
  • src/lib/shields/timer.ts

Comment on lines +119 to +127
vi.spyOn(fs.promises, "mkdir").mockImplementation(async (targetPath, options) => {
if (String(targetPath) === lifecycleDirectory && setupFailuresRemaining > 0) {
setupFailuresRemaining -= 1;
const error = new Error("simulated pre-fence setup failure") as NodeJS.ErrnoException;
error.code = "EIO";
throw error;
}
return await originalMkdir(targetPath, options);
});

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the conditional fault injection from this test file to unblock CI.

The Codebase Growth Guardrails job fails on this file: "test file contains 4 if statements, up from 0." The four if statements are the path checks at Line 120, Line 156, Line 193, and Line 204. The guardrail counts them even though they sit inside mockImplementation bodies.

Move the path-matched fault injection into a shared test helper outside this file, so the conditional lives in helper code and the test bodies stay linear. A helper such as failFsCallForPath(target, method, errorCode) in test/helpers/ keeps every call site a single expression.

Also applies to: 155-165, 192-201, 203-212

🤖 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-recovery-budget.test.ts` around lines 119 - 127, Remove
the path-based conditional fault-injection logic from the mockImplementation
blocks in the timer-recovery budget tests. Add or reuse a shared helper such as
failFsCallForPath in test/helpers to encapsulate matching the target path,
tracking failure attempts, creating the configured filesystem error, and
delegating other calls; update the affected mkdir and filesystem mock call sites
to use that helper as single expressions while preserving existing failure
behavior.

Source: Pipeline failures

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

Re-reviewed exact head f0f888b. The prior blocking unbounded-recovery defect is resolved: one seven-attempt budget now covers deadline-fence setup, main-generation publication, and restore/re-lock attempts. On exhaustion, durable containment becomes the terminal state and owned deadline/main gates are released when that containment is proven; if containment publication itself fails, the exact owned gates are retained with actionable recovery guidance. Exact-head focused evidence passed the new recovery-budget suite 5/5, lifecycle-lock acquisition 28/28, CLI build/typecheck, and 21/22 timer tests; the sole local timer failure was the sandboxed Darwin process-identity fixture, while the revision macOS check is green. The failed CLI shard is an unrelated managed-image-registry fetch timeout. The remaining growth-guardrail failure is an automated test-shape issue, not a product-blocking defect. No blocking correctness, security, regression, or compatibility issue found.

@jyaunches
jyaunches enabled auto-merge (squash) August 4, 2026 15:32
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
@jyaunches
jyaunches merged commit 8683777 into main Aug 4, 2026
88 of 94 checks passed
@jyaunches
jyaunches deleted the codex/shields-deadline-safety-7952 branch August 4, 2026 16:26
apurvvkumaria added a commit that referenced this pull request Aug 4, 2026
<!-- markdownlint-disable MD041 -->
## Summary

Prepares the canonical v0.0.102 release documentation from the current
release-labeled scope.
The change adds a dated changelog for all 38 user-facing shipping PRs
and corrects the OpenClaw agent command reference for the behavior
delivered by #8191.

## Changes

- Add `docs/changelog/2026-08-04.mdx` with the v0.0.102 release summary,
detailed behavior changes, support boundaries, security evidence links,
and links to durable documentation.
- Update `docs/reference/commands.mdx` to describe non-JSON OpenClaw
output capture, its combined limit, marker handling, stream suppression,
recovery guidance, and exit behavior.
- [#8167](#8167) ->
`docs/changelog/2026-08-04.mdx`: Records authenticated attachment of
operator-managed llama.cpp servers.
- [#8129](#8129) ->
`docs/changelog/2026-08-04.mdx`: Records the Experimental managed vLLM
profile for two DGX Spark systems.
- [#7983](#7983) ->
`docs/changelog/2026-08-04.mdx`: Records qualification of the May 2026
GB300WS factory image.
- [#8207](#8207) ->
`docs/changelog/2026-08-04.mdx`: Records the qualified DGX Station
driver transaction.
- [#8208](#8208) ->
`docs/changelog/2026-08-04.mdx`: Records mode-bound Express resume
state.
- [#8158](#8158) ->
`docs/changelog/2026-08-04.mdx`: Records recovery of host-global
dual-Station runtime ownership.
- [#8145](#8145) ->
`docs/changelog/2026-08-04.mdx`: Records Windows-host Ollama validation
from Docker Desktop's network context.
- [#8190](#8190) ->
`docs/changelog/2026-08-04.mdx`: Records HTTP model pulls when WSL has
no local Ollama executable.
- [#8195](#8195) ->
`docs/changelog/2026-08-04.mdx`: Records reuse of a healthy
installer-managed CLI.
- [#8053](#8053) ->
`docs/changelog/2026-08-04.mdx`: Records early rejection of incompatible
OpenShell gateway versions.
- [#8098](#8098) ->
`docs/changelog/2026-08-04.mdx`: Records the bounded
package-service-to-standalone gateway recovery transition.
- [#8216](#8216) ->
`docs/changelog/2026-08-04.mdx`: Records the final dashboard port
selected during multi-sandbox onboarding.
- [#8146](#8146) ->
`docs/changelog/2026-08-04.mdx`: Records managed startup-state
restoration for stopped sandboxes.
- [#8092](#8092) ->
`docs/changelog/2026-08-04.mdx`: Records gateway watchdog recovery for
classified not-serving states.
- [#8182](#8182) ->
`docs/changelog/2026-08-04.mdx`: Records consistent managed-recovery
wait configuration.
- [#8040](#8040) ->
`docs/changelog/2026-08-04.mdx`: Records Docker sandbox rollback
authority through late validation.
- [#8130](#8130) ->
`docs/changelog/2026-08-04.mdx`: Records bounded Shields deadline
recovery and durable containment.
- [#8086](#8086) ->
`docs/changelog/2026-08-04.mdx`: Records repair of narrowly validated
permission-only configuration drift.
- [#8122](#8122) ->
`docs/changelog/2026-08-04.mdx`: Records prompt failure and guidance for
corrupt transition locks.
- [#8124](#8124) ->
`docs/changelog/2026-08-04.mdx`: Records policy restoration flags,
previews, and target revalidation.
- [#7886](#7886) ->
`docs/changelog/2026-08-04.mdx`: Records explicit destruction after
pre-delete Shields hardening failures while preserving recovery
authority.
- [#7901](#7901) ->
`docs/changelog/2026-08-04.mdx`: Records multi-port uninstall behavior
and shared-resource preservation.
- [#7984](#7984) ->
`docs/changelog/2026-08-04.mdx`: Records one classified transient remote
MCP startup retry.
- [#7954](#7954) ->
`docs/changelog/2026-08-04.mdx`: Records bounded hosted-inference probe
replies.
- [#7574](#7574) ->
`docs/changelog/2026-08-04.mdx`: Records preservation of validated
reasoning capabilities through onboarding.
- [#8089](#8089) ->
`docs/changelog/2026-08-04.mdx`: Records proxy routing for Hermes
WhatsApp pairing and media traffic.
- [#7682](#7682) ->
`docs/changelog/2026-08-04.mdx`: Records native Hermes session deletion
and identifier validation.
- [#8150](#8150) ->
`docs/changelog/2026-08-04.mdx`: Records corporate CA trust for
LangChain Deep Agents Code image builds.
- [#8156](#8156) ->
`docs/changelog/2026-08-04.mdx`: Records reviewed managed runtime
dependency remediation.
- [#8180](#8180) ->
`docs/changelog/2026-08-04.mdx`: Records reviewed MCP discovery runtime
dependency updates.
- [#8196](#8196) ->
`docs/changelog/2026-08-04.mdx`: Records private npm dependency
remediation across managed images.
- [#8203](#8203) ->
`docs/changelog/2026-08-04.mdx`: Records reviewed Hermes and LangChain
Deep Agents Code Python dependency updates.
- [#8125](#8125) ->
`docs/changelog/2026-08-04.mdx`: Records bounded diagnostics for invalid
enumerated CLI values.
- [#8193](#8193) ->
`docs/changelog/2026-08-04.mdx`: Records bounded diagnostics for
unresolved sandbox base images.
- [#8118](#8118) ->
`docs/changelog/2026-08-04.mdx`: Records bounded diagnostics for changed
gateway authority.
- [#8191](#8191) ->
`docs/changelog/2026-08-04.mdx`, `docs/reference/commands.mdx`: Records
output capture, marker handling, recovery guidance, and exit behavior
for non-JSON OpenClaw agent commands.
- [#8187](#8187) ->
`docs/changelog/2026-08-04.mdx`: Records the aligned
interactive-installation start across supported agents.
- [#8153](#8153) ->
`docs/changelog/2026-08-04.mdx`: Records current product capabilities
and support boundaries.

## Type of Change

- [ ] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [x] 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:
- [x] Tests not applicable — justification: This documentation-only
release preparation does not change executable behavior. Existing
changelog and published-route tests pass.
- [x] 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:
- [ ] 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: Independently reviewed `docs/changelog/2026-08-04.mdx` and
`docs/reference/commands.mdx` at commit `b89913780`. All 38 user-facing
v0.0.102 PRs are represented, #8191 behavior matches the implementation,
and the writing rules, documentation style, controlled terminology,
route structure, and skip policy pass review. Targeted tests pass 36/36
and the documentation build completes with 0 errors.
- Agent: Codex Desktop independent documentation writer
<!-- docs-review-head-sha: b899137 -->
<!-- docs-review-agents-blob-sha: 3dd7c24 -->

## DGX Station Hardware Evidence

- [ ] Tested on DGX Station
- Tested commit: Not applicable
- 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 — `npx vitest run --project integration
test/changelog-docs.test.ts test/check-docs-published-routes.test.ts`
passed 36/36.
- [x] Applicable broad gate passed — not applicable to
documentation-only changes; `npm run docs` completed successfully with 0
errors.
- [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) —
completed with 0 errors and 2 existing Fern warnings.
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [x] New doc pages include SPDX header and frontmatter (new pages only)
— the native dated changelog uses the required parser-safe MDX SPDX
comment and intentionally has no frontmatter.

---
Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
- Added release notes for v0.0.102, covering authentication, hardware
setup, WSL, installer recovery, sandbox resilience, policy management,
inference reliability, CLI improvements, and unified quickstarts.
- Updated command documentation to explain how non-JSON agent output is
collected, replayed, and reported.

- **Bug Fixes**
- Improved command-output recovery guidance when output exceeds limits
or contains unsupported fallback markers.
- Preserved accurate command exit-status reporting after output
processing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
jyaunches added a commit that referenced this pull request Aug 5, 2026
<!-- markdownlint-disable MD041 -->
## 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:

- #7761 added the helper containing Shields up, gateway restart, Shields
down, and rollback, but did not run the complete live E2E.
- #7771 upgraded Hermes, while its selected E2E did not include the MCP
bridge target.
- #7849 fixed Hermes 0.19 migrations and the `mcp__fake__*` tool naming,
allowing the journey to progress far enough to expose the later failure.
- #7866 moved an explicit restart of A before the post-removal call,
which reapplied A's policy and masked the defect.

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)
- [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 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

- [x] 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
<!-- docs-review-head-sha: 5d46d4a -->
<!-- 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
- [ ] 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`.
- [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) — GitHub
CI is running for commit `5d46d4a2ee924edf743ac36d04807948eec01c96`.
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [x] New doc pages include SPDX header and frontmatter (new pages only)
— no new pages

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Co-authored-by: Carlos Villela <cvillela@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 area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery area: security Security controls, permissions, secrets, or hardening bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants