Skip to content

fix(sandbox): preserve custom policy presets across snapshot restore#5104

Merged
cv merged 1 commit into
NVIDIA:mainfrom
latenighthackathon:fix/snapshot-restore-custom-policies
Jun 11, 2026
Merged

fix(sandbox): preserve custom policy presets across snapshot restore#5104
cv merged 1 commit into
NVIDIA:mainfrom
latenighthackathon:fix/snapshot-restore-custom-policies

Conversation

@latenighthackathon

@latenighthackathon latenighthackathon commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

A sandbox with custom policy presets applied via policy --from-file / --from-dir lost them after snapshot restore (including snapshot restore --to <dst>): the restored sandbox kept its built-in presets but none of the custom ones.

snapshot create records applied presets into the rebuild manifest so they survive destroy/recreate, because presets live in the gateway policy engine rather than on the sandbox filesystem. But it only captured the built-in names (policyPresets, from registry.policies). Custom presets are tracked separately in registry.customPolicies and live in the same gateway-only location, so they were subject to the same loss but were never captured or replayed.

Each CustomPolicyEntry already stores the preset's full content, so the data needed to replay is available — it just was not carried in the manifest.

Related Issue

Closes #5103.

Changes

  • src/lib/state/sandbox.ts: add customPolicies?: CustomPolicyEntry[] to RebuildManifest (+ validator), and capture sb.customPolicies in createBackup alongside policyPresets.
  • src/lib/actions/sandbox/snapshot.ts: on restore, reconcile custom policies the same way built-in presets are reconciled — re-apply snapshot entries by content via applyPresetContent (which re-records them in the target registry) and remove any the snapshot did not record.
  • test/snapshot.test.ts: cover that customPolicies round-trips through the manifest and that a malformed entry (missing content) is rejected by the manifest validator.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Verification

  • npx vitest run --project cli test/snapshot.test.ts test/rebuild-policy-presets.test.ts test/policies.test.ts test/snapshot-restore-existing-dest.test.ts — 207/207 pass
  • npm run typecheck:cli and npm run build:cli — clean

Signed-off-by: latenighthackathon latenighthackathon@users.noreply.github.com

Summary by CodeRabbit

  • New Features

    • Custom policy presets are now captured in sandbox snapshots and automatically reconciled/restored when loading a snapshot.
    • Snapshot manifest now optionally includes custom policy metadata so these policies persist across backups.
  • Tests

    • Added tests to confirm custom policies are included in backups and that malformed entries are ignored.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7ccb5df6-ff08-43d9-8611-9da058f249eb

📥 Commits

Reviewing files that changed from the base of the PR and between a6f7e14 and ceddddc.

📒 Files selected for processing (2)
  • src/lib/actions/sandbox/snapshot.ts
  • src/lib/state/sandbox.ts
💤 Files with no reviewable changes (1)
  • src/lib/state/sandbox.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/actions/sandbox/snapshot.ts

📝 Walkthrough

Walkthrough

Backup now captures sandbox customPolicies into the rebuild manifest; manifest validation accepts only well-formed entries. On restore, the snapshot's customPolicies are reconciled against the target registry (remove absent, re-apply missing by content). Tests cover listing and malformed-manifest behavior.

Changes

Custom Policy Snapshot Capture and Restore

Layer / File(s) Summary
Manifest schema and validation
src/lib/state/sandbox.ts
Adds CustomPolicyEntry import and optional customPolicies?: CustomPolicyEntry[] to RebuildManifest; adds isCustomPolicyEntryArray and integrates it into isRebuildManifest.
Backup capture of custom policies
src/lib/state/sandbox.ts
backupSandboxState reads sb.customPolicies, copies entries into a captured array, logs names, and includes customPolicies in the serialized manifest.
Snapshot restore reconciliation
src/lib/actions/sandbox/snapshot.ts
After restore, diffs resolvedSnapshot.customPolicies vs target registry by name, removes extras, and re-applies missing or differing entries via policies.applyPresetContent using stored content and sourcePath; collects per-entry warnings.
Backup listing and validation tests
test/snapshot.test.ts
Adds tests: one verifies listBackups returns captured customPolicies; another verifies manifests with malformed customPolicies (missing content) are ignored.

Sequence Diagram

sequenceDiagram
  participant SnapshotRestore
  participant RebuildManifest
  participant Registry
  participant PolicyEngine

  SnapshotRestore->>RebuildManifest: read resolvedSnapshot.customPolicies
  SnapshotRestore->>Registry: list current customPolicies by name
  SnapshotRestore->>Registry: remove policy(name) for each extra
  SnapshotRestore->>PolicyEngine: policies.applyPresetContent(content, { custom: { sourcePath } })
  PolicyEngine-->>Registry: persist applied custom policy entry
  Registry-->>SnapshotRestore: return removal/apply results (accumulate warnings)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • NVIDIA/NemoClaw#5101: Modifies the same runSandboxSnapshot restore flow and adds a permission repair step that runs earlier in the restore sequence.

Suggested reviewers

  • cv

"🐰 I hopped through manifests with care,
capturing presets found hiding there.
On restore I nudge registry and pen,
reapply content so policies live again.
A little patch, a safer land—hooray for every sandboxed den!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preserving custom policy presets across snapshot restore, which matches the primary change in the changeset.
Linked Issues check ✅ Passed The code changes fully implement the required fix: capturing customPolicies in RebuildManifest with validation, recording them in backups, and reconciling them on restore by re-applying and removing unmatched entries.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objectives: extending RebuildManifest, updating backup/restore logic, and adding validating tests for customPolicies round-trip and malformed entry rejection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/actions/sandbox/snapshot.ts`:
- Around line 604-609: The current reconciliation only diffs snapshotCustom vs
currentCustom by name (variables toRemove/toAdd), so policies with the same name
but different content won’t be updated; change the comparison to detect content
differences (e.g., deep equality or a deterministic JSON/string hash of the
policy body) and treat a name-matching but content-different entry as needing
replacement: include it in toRemove and toAdd (or call an update path). Update
the logic that builds toRemove/toAdd (references: snapshotCustom, currentCustom,
toRemove, toAdd) to compare full policy content (not just .name) so restore
re-applies snapshot policy bodies; use a canonical serializer or deepEqual
helper to avoid false positives from key ordering.

In `@src/lib/state/sandbox.ts`:
- Around line 1011-1013: The code currently omits the customPolicies field when
sb.customPolicies is empty, causing snapshots with zero policies to be
indistinguishable from legacy snapshots and preventing reconciliation; change
the initialization of customPolicies (the CustomPolicyEntry[] assigned from sb)
to always persist the property even when empty (i.e., set customPolicies to an
empty array when sb.customPolicies is undefined/null or length 0) so restore
logic can detect an explicit empty list; update the declaration that reads from
sb (and any similar occurrences) and keep the _log call as-is to reflect the
empty array.
🪄 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: 45b2d990-094c-43f2-ac4a-ddc43f9e6ecf

📥 Commits

Reviewing files that changed from the base of the PR and between bf817c4 and 70fd6f6.

📒 Files selected for processing (3)
  • src/lib/actions/sandbox/snapshot.ts
  • src/lib/state/sandbox.ts
  • test/snapshot.test.ts

Comment thread src/lib/actions/sandbox/snapshot.ts Outdated
Comment thread src/lib/state/sandbox.ts Outdated
@latenighthackathon latenighthackathon force-pushed the fix/snapshot-restore-custom-policies branch from 70fd6f6 to a6f7e14 Compare June 10, 2026 03:23
@wscurran wscurran added area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery bug-fix PR fixes a bug or regression labels Jun 10, 2026
@wscurran

Copy link
Copy Markdown
Contributor

@prekshivyas prekshivyas self-assigned this Jun 10, 2026

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

Reviewed (code + security checklist). Captures custom --from-file/--from-dir policy presets (name + content) into the rebuild manifest and reconciles them on snapshot restore, mirroring the existing built-in policyPresets path. Fixes #5103.

Approve. Verified the load-bearing pieces against source rather than the diff:

  • CustomPolicyEntry + getCustomPolicies() (state/registry.ts), applyPresetContent(..., {custom}) + removePreset (policy/index.ts) exist and match the call sites; the manifest validator (isCustomPolicyEntryArray) is wired and rejects entries missing content.
  • The reconcile is full-replacement — it restores exactly the snapshotted presets and removes any not in the snapshot, so it can't grant egress beyond what was already captured. It also fixes a latent posture bug where custom policy narrowing silently vanished on restore. Security: all 9 categories pass (name-only logging, no content leakage; manifests are local, same trust boundary as the snapshot).

Non-blocking notes:

  • Same-name / different-content presets aren't reconciled (the add/remove keys on name only) — so an edited-in-place custom preset keeps the live content rather than restoring the snapshot's. This matches the built-in reconcile's name-only semantics (built-ins can't diverge, custom content can), so it's a known limitation, not a regression. A one-line code comment noting the gap would help; a content !== re-apply would close it if you want.
  • PR body says the capture is in createBackup; the function is actually backupSandboxState.
  • The custom reconcile block closely duplicates the built-in one — extractable, but they diverge on apply-by-name vs apply-by-content, so duplication is tolerable.

Tests adequate (manifest round-trip + malformed-entry rejection); an explicit same-name-different-content test would document the gap above.

snapshot create/restore reconciled built-in policy presets (policyPresets)
but dropped custom presets applied via `policy --from-file`/`--from-dir`.
Custom presets live only in the gateway policy engine, not on the sandbox
filesystem, so restore (including `--to <dst>`) silently lost them: the
restored sandbox kept its built-in presets but none of the custom ones.

Capture customPolicies (each CustomPolicyEntry carries full content) into the
rebuild manifest and reconcile them on restore the same way policyPresets are:
re-apply by content via applyPresetContent (which re-records them in the target
registry) and remove any custom presets the snapshot did not record.

Reconcile by content, not only by name: a same-name preset whose body or
source path changed is re-applied. Always persist customPolicies in the
manifest (even an empty array) so restore can tell a zero-custom snapshot
(reconcile, removing stale custom presets on the target) from a legacy snapshot
that predates the field (skip reconcile).

Closes NVIDIA#5103

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon latenighthackathon force-pushed the fix/snapshot-restore-custom-policies branch from a6f7e14 to ceddddc Compare June 11, 2026 16:43
@prekshivyas prekshivyas added the v0.0.64 Release target label Jun 11, 2026
@cv cv merged commit e88119a into NVIDIA:main Jun 11, 2026
30 checks passed
cv pushed a commit that referenced this pull request Jun 12, 2026
## Summary
- Add v0.0.64 release notes from the release announcement and link them
to the relevant deeper docs.
- Document that custom policy presets recorded through `policy-add
--from-file` and `--from-dir` survive snapshot restore and sandbox
recreation.
- Refresh generated NemoClaw user skills from the current source docs.

## Source summary
- #5104 -> `docs/manage-sandboxes/backup-restore.mdx`,
`docs/network-policy/customize-network-policy.mdx`: Documents custom
policy presets preserved through snapshot restore.
- #4955 -> `docs/about/release-notes.mdx`: Adds release-note coverage
for Brave web-search pinning and `BRAVE_API_KEY` placeholder
preservation.
- #5116, #5269 -> `docs/about/release-notes.mdx`: Adds release-note
coverage for Docker-driver gateway health and rootfs guard stability.
- #5241, #5085 -> `docs/about/release-notes.mdx`: Adds release-note
coverage for chat-completions provider selection and Nemotron Ultra 550B
tool-less request compatibility.
- #5268, #5210, #5257 -> `docs/about/release-notes.mdx`: Adds
release-note coverage for messaging render plan refresh, OpenClaw
scope-upgrade approval recovery, and Hermes WhatsApp bridge dependency
setup.
- Current source docs -> `.agents/skills/`: Regenerates user-skill
references so agent-facing guidance matches the source documentation.

## Verification
- `python3 scripts/docs-to-skills.py docs/ .agents/skills/ --prefix
nemoclaw-user --doc-platform fern-mdx`
- `npm run docs`
- `npm run build:cli`
- `npm run typecheck:cli`
- Commit/pre-push hooks: markdownlint, gitleaks, docs-to-skills
verification, TypeScript CLI, and skills YAML checks passed.

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

* **Documentation**
* Clarified sandbox snapshot restore preserves custom policy presets and
restores them without original files.
* Switched sandbox setup and remote deployment guidance to Docker-based
workflows and emphasized remote onboarding flow.
* Expanded troubleshooting for gateway recovery, Docker GPU/WSL issues,
and onboarding resume.
* Added/updated CLI docs: advanced maintenance, session export,
upload/download wrappers, and status recovery guidance.
* Added v0.0.64 release notes and links to NemoClaw Community; fixed
command reference formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

snapshot restore drops custom policy presets (--from-file/--from-dir): customPolicies not captured in the rebuild manifest

4 participants