Skip to content

fix(snapshot): refuse restore --to existing destination; add --force opt-in#3796

Open
laitingsheng wants to merge 9 commits into
mainfrom
fix/snapshot-restore-existing-destination
Open

fix(snapshot): refuse restore --to existing destination; add --force opt-in#3796
laitingsheng wants to merge 9 commits into
mainfrom
fix/snapshot-restore-existing-destination

Conversation

@laitingsheng
Copy link
Copy Markdown
Contributor

@laitingsheng laitingsheng commented May 19, 2026

Summary

snapshot restore --to <dst> used to overlay onto an existing destination's filesystem silently. Now it refuses by default; --force deletes the destination and recreates it from the source's image, with interactive confirmation unless --yes or NEMOCLAW_NON_INTERACTIVE=1 is set.

Related Issue

Fixes #3756

Changes

  • Refuse snapshot restore --to <dst> when dst already exists. The refusal fires before source-side preflight, so the user always sees the precise "destination exists" error even when the source is also missing or its image cannot be resolved.
  • Add --force (delete-then-recreate) and --yes/-y (skip prompt) flags to the typed runSandboxSnapshot({ kind: "restore", ... }) action and to the oclif command.
  • Validate the snapshot selector and source pod image before deleting the destination, so a bad selector or unresolvable image leaves dst intact.
  • Delete sequence stops the destination's NIM container, runs openshell sandbox delete, then performs the destination-only cleanups sandboxDestroy does — /tmp/nemoclaw-services-<dst>, per-sandbox messaging providers (<dst>-telegram-bridge, -discord-bridge, -slack-bridge, -slack-app, -wechat-bridge), and shields state — before removing the registry entry. Host-shared cleanups (Ollama proxy, Ollama model unload, host services, gateway teardown) are intentionally skipped because they would also affect the source sandbox we are about to clone from.
  • Update docs/reference/commands.mdx with the new flags, the refuse-by-default behaviour, and a fix to the long-standing inaccurate "exact or prefix timestamp" claim (findBackup matches exact timestamps only).
  • Update the snapshot help fallback usage text in runSandboxSnapshot so it lists --force / --yes.
  • Seven integration tests in test/snapshot-restore-existing-dest.test.ts covering refuse-default; refuse-default-even-when-source-image-broken (locks the ordering); --force --yes reaches the delete step; --force under NEMOCLAW_NON_INTERACTIVE=1 skips the prompt; no-delete when no snapshot, bad selector, or unresolvable source image. The --force assertions verify the delete step ran; the subsequent sandbox create is mocked to exit non-zero rather than mocking the full create stream.

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)

Verification

  • npx prek run --all-files passes
  • npm test passes (one pre-existing flake in e2e-scenario-framework unrelated to this PR)
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Tinson Lai tinsonl@nvidia.com

Summary by CodeRabbit

  • Documentation

    • Updated CLI reference title/sidebar; added detailed docs for the new exec subcommand and expanded snapshot restore docs with new flags, behavior, and examples.
  • New Features

    • Added a non-interactive exec command to run a single command inside a running sandbox.
    • Added --force and --yes|-y to snapshot restore to allow overwriting existing destinations and skipping confirmation.
  • Tests

    • Added regression tests for restore behavior with existing destinations, non-interactive mode, and preflight validations.

Review Change Stack

…opt-in

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented May 19, 2026

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

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: ed2ddeb9-8428-4204-97b1-758a8d4b99b6

📥 Commits

Reviewing files that changed from the base of the PR and between c31f120 and fd2acb2.

📒 Files selected for processing (1)
  • src/lib/actions/sandbox/snapshot.ts
💤 Files with no reviewable changes (1)
  • src/lib/actions/sandbox/snapshot.ts

📝 Walkthrough

Walkthrough

Adds --force and --yes|-y to nemoclaw <name> snapshot restore; pre-resolves selector and source image before any destination deletion; refuses cross-sandbox restores into an existing destination unless --force is used; --force prompts (skippable with --yes or NEMOCLAW_NON_INTERACTIVE=1) and performs destination-only deletion; includes unit and regression tests and docs (plus a new exec doc section).

Changes

Cross-sandbox snapshot restore with force/yes flags

Layer / File(s) Summary
CLI flag definition and wiring
src/commands/sandbox/snapshot/restore.ts, src/lib/cli/public-display-defaults.ts
Two new boolean flags (--force and `--yes
Restore action types and decision logic
src/lib/actions/sandbox/snapshot.ts
SnapshotRequest type extended with force and yes; adds deleteSandboxForRestore helper; preflight refactored to compute cross-sandbox/target-exists early and resolve selector/source image before destructive actions; implements refuse-unless-force, interactive confirmation (skippable), and delete-then-recreate flow.
Regression test suite
test/snapshot-restore-existing-dest.test.ts
Adds CLI runner and mock environment factory plus tests verifying refusal when destination exists, preflight-before-delete ordering, deletion with --force --yes, non-interactive bypass via env var, and that preflight failures abort without deleting the destination.
Command unit test updates
src/commands/sandbox/snapshot.test.ts
Restore command tests updated to assert force and yes fields in the action options (undefined when not passed, true when flags set).
Documentation and CLI help updates
docs/reference/commands.mdx
Frontmatter title/sidebar updated; new nemoclaw <name> exec section added; snapshot restore signature extended with `[--force] [--yes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as SnapshotRestoreCommand
  participant Action as SnapshotRestoreAction
  participant OpenShell as OpenShell
  participant FS as HostFilesystem

  CLI->>Action: request restore {selector,to,force,yes}
  Action->>Action: preflight (resolve selector, resolve source image, check targetExists)
  alt target exists and not force
    Action->>CLI: error "destination exists"
  else force
    Action->>CLI: interactive confirm (skippable by yes/ENV)
    CLI->>Action: confirmation
    Action->>OpenShell: sandbox delete <dst>
    OpenShell-->>Action: delete outcome
    Action->>FS: remove per-sandbox artifacts
    Action->>Action: autoCreateSandboxFromSource(...)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

Sandbox, enhancement: feature

Suggested reviewers

  • cv
  • ericksoa

Poem

🐰 I hopped in quick to guard the gate,
A --force to change, a --yes to sate.
Preflight checks before the knife,
Delete with promise, restore with life.
🥕 Safe sandboxes, no surprise.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 'fix(snapshot): refuse restore --to existing destination; add --force opt-in' accurately describes the main change: adding rejection of existing destinations by default and introducing a --force flag to opt-in to overwrite behavior.
Linked Issues check ✅ Passed The PR fully addresses issue #3756 by implementing explicit existence checks that refuse restore when destination exists, adding --force flag for opt-in deletion, requiring confirmation unless --yes or NEMOCLAW_NON_INTERACTIVE=1, validating source before deletion, and documenting all changes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the #3756 requirements: CLI flags, command behavior, helper functions, tests, and documentation updates all support the core objective of refusing existing destinations and adding --force override.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/snapshot-restore-existing-destination

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

E2E Advisor Recommendation

Required E2E: snapshot-commands-e2e
Optional E2E: sandbox-operations-e2e, state-backup-restore-e2e

Dispatch hint: snapshot-commands-e2e

Auto-dispatched E2E: snapshot-commands-e2e via nightly-e2e.yaml at fd2acb236ee67ab320577fd633425f4239b6cf14nightly run

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • snapshot-commands-e2e (medium (~30 min timeout, cloud/API-key backed sandbox)): This is the targeted existing E2E for the snapshot create/list/restore lifecycle. The changed restore implementation directly affects live snapshot restore behavior and user workspace recovery, so this should block merge.

Optional E2E

  • sandbox-operations-e2e (high (~60 min timeout)): Useful adjacent coverage because the new forced restore path deletes and recreates a sandbox and performs registry/provider/shields cleanup. This job validates broader sandbox lifecycle, destroy/recovery, registry rebuild, and multi-sandbox isolation behavior, but it is less targeted than snapshot-commands-e2e.
  • state-backup-restore-e2e (high (~60 min timeout)): Adjacent confidence for persisted workspace backup/restore semantics and data-preservation paths. The PR changes snapshot restore, not the generic backup-workspace lifecycle, so this is useful but not merge-blocking.

New E2E recommendations

  • snapshot restore to existing destination (high): Existing snapshot-commands-e2e covers create/list/in-place restore and timestamp restore, but it does not appear to exercise snapshot restore --to <existing-dst> refusal, --force --yes delete-and-recreate, or the preflight guarantees that a bad selector/unresolvable source image must not delete the destination.
    • Suggested test: Extend test/e2e/test-snapshot-commands.sh or add a scenario-suite step that creates a second sandbox, verifies restore-to-existing-destination refuses by default, verifies --force --yes recreates/restores into the destination, and verifies source/destination state isolation after restore.

Dispatch hint

  • Workflow: nightly-e2e.yaml
  • jobs input: snapshot-commands-e2e

…force destination

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@github-actions
Copy link
Copy Markdown
Contributor

…existing-destination

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>

# Conflicts:
#	docs/reference/commands.md
#	src/lib/actions/sandbox/snapshot.ts
#	src/lib/commands/sandbox/snapshot.test.ts
#	src/lib/commands/sandbox/snapshot/restore.ts
…rtifacts on --force

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@laitingsheng laitingsheng marked this pull request as ready for review May 20, 2026 04:03
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
docs/reference/commands.mdx (1)

838-842: ⚡ Quick win

Replace colon with period or em dash.

Line 839 uses a colon between two independent clauses rather than to introduce a list. As per coding guidelines, colons should only introduce lists, not serve as general punctuation between clauses.

✏️ Suggested revision

Replace the colon with a period to form two sentences, or use an em dash if you want to preserve the close relationship between the clauses:

Option 1 (two sentences):

-To overwrite an existing destination, pass `--force`: the command deletes `dst`, then recreates it from the source's image and restores the snapshot into the fresh copy.
+To overwrite an existing destination, pass `--force`. The command deletes `dst`, then recreates it from the source's image and restores the snapshot into the fresh copy.

Option 2 (em dash):

-To overwrite an existing destination, pass `--force`: the command deletes `dst`, then recreates it from the source's image and restores the snapshot into the fresh copy.
+To overwrite an existing destination, pass `--force` — the command deletes `dst`, then recreates it from the source's image and restores the snapshot into the fresh copy.

As per coding guidelines: "Colons should only introduce a list. Flag colons used as general punctuation between clauses."

🤖 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 838 - 842, In the sentence that
begins "To overwrite an existing destination, pass `--force`: the command
deletes `dst`, then recreates it..." replace the colon after "`--force`" with
either a period (split into two sentences) or an em dash to avoid using a colon
between independent clauses; update the text that follows "`--force`"
accordingly so it reads either "To overwrite an existing destination, pass
`--force`. The command deletes `dst`, then recreates it..." or "To overwrite an
existing destination, pass `--force` — the command deletes `dst`, then recreates
it..." keeping the rest of the wording unchanged.
🤖 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 `@test/snapshot-restore-existing-dest.test.ts`:
- Around line 28-51: Refactor runCli to avoid command-injection by changing its
signature to accept args: string[] and use child_process.execFileSync (or
execFileSync) with CLI as the executable and the args array (preserving
encoding, timeout, and env merging including NEMOCLAW_* vars); update the
function body to handle the thrown error shape exactly as before but using
execFileSync's invocation, and update every call site in this test file that
passes a single string to runCli to instead pass an array of argument strings
(e.g., ["snapshot", "restore", ...]) so no shell interpolation is used.

---

Nitpick comments:
In `@docs/reference/commands.mdx`:
- Around line 838-842: In the sentence that begins "To overwrite an existing
destination, pass `--force`: the command deletes `dst`, then recreates it..."
replace the colon after "`--force`" with either a period (split into two
sentences) or an em dash to avoid using a colon between independent clauses;
update the text that follows "`--force`" accordingly so it reads either "To
overwrite an existing destination, pass `--force`. The command deletes `dst`,
then recreates it..." or "To overwrite an existing destination, pass `--force` —
the command deletes `dst`, then recreates it..." keeping the rest of the wording
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 494004ba-7fa4-4781-9ada-07eeb065f46a

📥 Commits

Reviewing files that changed from the base of the PR and between 11b1937 and dd65755.

📒 Files selected for processing (5)
  • docs/reference/commands.mdx
  • src/commands/sandbox/snapshot.test.ts
  • src/commands/sandbox/snapshot/restore.ts
  • src/lib/actions/sandbox/snapshot.ts
  • test/snapshot-restore-existing-dest.test.ts

Comment thread test/snapshot-restore-existing-dest.test.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26140579903
Target ref: dd657556d3b017f97d2e0efcc21399c46b880984
Workflow ref: main
Requested jobs: snapshot-commands-e2e
Summary: 1 passed, 0 failed, 0 skipped

Job Result
snapshot-commands-e2e ✅ success

…dest helper

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@github-actions
Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26140953271
Target ref: 4a98876bfb9b4bfe61cef8008d8ccb919403f7a1
Workflow ref: main
Requested jobs: snapshot-commands-e2e
Summary: 1 passed, 0 failed, 0 skipped

Job Result
snapshot-commands-e2e ✅ success

@laitingsheng laitingsheng added v0.0.47 Release target NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). labels May 20, 2026
- src/commands/sandbox/snapshot/restore.ts: drop the local
  publicDisplay static (main moved this metadata into the central
  public-display-defaults registry); keep --force/--yes flags and
  examples.
- src/lib/cli/public-display-defaults.ts: update sandbox:snapshot:restore
  flags string to include --force and --yes|-y.
- src/lib/actions/sandbox/snapshot.ts: keep deleteSandboxForRestore from
  this branch, drop the orphaned probeDockerDriverGatewayRunning, and
  take main's probeGatewayMetadataHealth (uses isGatewayHealthy).

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Comment thread src/lib/actions/sandbox/snapshot.ts Fixed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

PR Review Advisor

Recommendation: blocked
Confidence: medium
Analyzed HEAD: fd2acb236ee67ab320577fd633425f4239b6cf14
Findings: 4 blocker(s), 2 warning(s), 0 suggestion(s)

This is an automated advisory review. A human maintainer must make the final merge decision.

Limitations: Review is based on trusted metadata, repository read-only inspection, and the provided diff; no scripts, tests, package-manager commands, or workflows were executed.; CI and E2E statuses may change after this review; current assessment is for head SHA fd2acb2 only.; No live sandbox was created or inspected, so filesystem, OpenShell registry, provider cleanup, and gateway behavior in real environments remain dependent on E2E results.; Review-thread state is taken from trusted GitHub metadata; the current file content no longer shows the stripAnsi import, but the thread is still reported unresolved.

Workflow run

Full advisor summary

PR Review Advisor

Base: origin/main
Head: HEAD
Analyzed SHA: fd2acb236ee67ab320577fd633425f4239b6cf14
Recommendation: blocked
Confidence: medium

PR addresses the linked snapshot restore safety issue, but merge is blocked by pending CI, BLOCKED merge state, an unresolved review thread, missing required E2E for the latest head SHA, and monolith growth in sandbox lifecycle code.

Gate status

  • CI: pending — Trusted GraphQL/status context for head SHA fd2acb2 reports 13 pending/in-progress/queued status contexts, including cli-parity, E2E recommendation, wsl-e2e, PR review advisor, CodeQL jobs, unit-vitest-linux, checks, ShellCheck, sandbox image builds, and CodeRabbit.
  • Mergeability: fail — Trusted GraphQL reports mergeStateStatus=BLOCKED for PR fix(snapshot): refuse restore --to existing destination; add --force opt-in #3796 at headRefOid fd2acb2.
  • Review threads: fail — Trusted GraphQL reports 1 unresolved review thread: CodeQL / unused import stripAnsi on src/lib/actions/sandbox/snapshot.ts.
  • Risky code tested: pass — Trusted path heuristics reported no risky code areas detected. Separately, runtime sandbox snapshot behavior still requires E2E validation because unit/mocked tests cannot fully prove live delete/recreate/restore semantics.

🔴 Blockers

  • Latest head is not through hard gates: The current head SHA is not merge-ready: CI has pending/in-progress/queued contexts, GitHub mergeStateStatus is BLOCKED, and reviewDecision is REVIEW_REQUIRED.
    • Recommendation: Wait for all required status contexts for fd2acb2 to complete successfully and re-check mergeability before considering merge.
    • Evidence: Trusted context: ci.status=pending with 13 pending contexts; mergeStateStatus=BLOCKED; reviewDecision=REVIEW_REQUIRED.
  • Unresolved review thread remains (src/lib/actions/sandbox/snapshot.ts:8): A hard gate is still failing because GitHub reports one unresolved review thread. The thread is a CodeQL comment about an unused stripAnsi import on src/lib/actions/sandbox/snapshot.ts. Although a later commit is named as dropping the unused import, the trusted review-thread state still reports it unresolved for this PR.
    • Recommendation: Resolve or otherwise disposition the outstanding review thread in GitHub after verifying the current diff no longer contains the unused import.
    • Evidence: Trusted GraphQL reviewThreads.nodes includes PRRT_kwDORnw8lM6DqmzX with isResolved=false and body 'CodeQL / Unused variable, import, function or class — Unused import stripAnsi.'
  • Required snapshot E2E is missing for latest head SHA: The E2E Advisor required snapshot-commands-e2e because this PR changes live snapshot restore and sandbox replacement behavior. The available successful E2E evidence targets older SHAs, not fd2acb2.
    • Recommendation: Ensure snapshot-commands-e2e passes for fd2acb2. Consider adding the Advisor-suggested live existing-destination restore coverage for refusal, preflight-no-delete, and --force --yes delete/recreate.
    • Evidence: E2E Advisor required snapshot-commands-e2e. Selective E2E success comments show target refs dd65755, 4a98876, and c31f120, but not fd2acb2.
  • Snapshot action monolith grew substantially in sandbox lifecycle code (src/lib/actions/sandbox/snapshot.ts:234): The main snapshot action file grew by 138 lines and now also contains forced destination deletion, provider cleanup, shields cleanup, and recreate orchestration. This is a security-sensitive sandbox lifecycle path and the trusted monolith budget flags this as a blocker.
    • Recommendation: Extract the forced destination replacement/delete cleanup logic into a focused module or offset the growth by moving existing helpers out of snapshot.ts before merge.
    • Evidence: Trusted monolithDeltas: src/lib/actions/sandbox/snapshot.ts baseLines=458, headLines=596, delta=138, severity=blocker. The new deleteSandboxForRestore helper and forced restore branch are in this file.

🟡 Warnings

🔵 Suggestions

  • None.

Acceptance coverage

  • met — [Description] When restoring a snapshot into an existing NemoClaw sandbox using --to, the documented Scenario B expects the operation to be rejected if the destination sandbox already exists: the command should fail with a clear error, exit non‑zero, and leave the destination sandbox unchanged.: The restore action checks targetExists && !request.force and emits Destination sandbox '' already exists before snapshot restoration; regression test 'refuses by default when the destination sandbox already exists' asserts exit code 1, destination-exists text, and no sandbox delete dst.
  • met — In practice, running nemoclaw new-sb snapshot restore cross-restore --to sbx-dst succeeds even when sbx-dst already exists, and reports ✓ Restored 13 directories, 0 files.: The default cross-sandbox existing-destination path now refuses unless --force is supplied, so the prior silent success path is blocked by default.
  • met — This shows that the restore operation proceeds into an existing destination instead of refusing to run, so the Scenario B “existing destination is rejected” behavior cannot be validated as written and there is a risk of silently mutating an existing sandbox’s filesystem.: The new implementation prevents default restore into an existing destination and documents/implements explicit --force for destructive replacement.
  • unknown — NemoClaw: v0.0.44 (same CLI version used for snapshot create/restore): The PR targets current source rather than reproducing v0.0.44. No runtime version execution was performed in this advisory review.
  • unknown — OpenShell CLI: 0.0.39: No OpenShell runtime was executed by this advisory review. The tests use fake openshell scripts.
  • partial — OpenClaw: 2026.4.24 (cbcfdf6): The test fixture manifest uses agentVersion '2026.4.24', but no live OpenClaw runtime was inspected.
  • unknown — Sandbox OS: Linux (default NemoClaw base image, x86_64): No live sandbox OS was created or inspected. Required E2E for the current head SHA is missing.
  • partial — Container runtime: Docker on DGX Spark host: The changed code includes Docker-driver handling and fake docker test coverage for gateway/image preflight, but no DGX Spark Docker runtime was inspected.
  • unknown — Network: standard outbound access; no networking issues involved in this scenario: The PR does not change network policy for this bug path, but no live network environment was exercised.
  • partial — NemoClaw CLI and OpenShell gateway are installed and working.: The restore code still probes gateway health and live sandbox list before acting; tests fake a connected gateway. No live install was verified.
  • met — Source sandbox new-sb exists and is running.: The cross-sandbox restore path requires liveNames.has(sandboxName) before auto-create or forced recreate. Tests fake src Ready in openshell sandbox list.
  • met — Destination sandbox sbx-dst has already been created and exists (this is intentional for Scenario B).: Tests fake both src and dst as Ready and exercise restore --to dst with dst already present.
  • partial — A snapshot named cross-restore exists for new-sb (e.g., nemoclaw new-sb snapshot create cross-restore was run earlier, producing version v1).: Tests create a snapshot manifest for src and cover latest snapshot plus missing selector cases, but they do not create/use the exact literal name cross-restore.
  • met — On the host, confirm that both sandboxes exist:: The restore action parses openshell sandbox list into liveNames; tests fake sandbox list output containing both src Ready and dst Ready.
  • met — and verify that new-sb and sbx-dst are both present.: The existing destination branch is driven by liveNames.has(targetSandbox). The tests verify behavior when both source and destination are listed.
  • partial — Confirm that new-sb has a snapshot named cross-restore:: The code resolves snapshots by version/name/timestamp via sandboxState.findBackup or latest via getLatestBackup. Tests cover a present latest snapshot and missing selector, but not the exact cross-restore name.
  • partial — and note the version (e.g., v1 name=cross-restore).: The code prints resolved snapshot version/name when a selector is found, but tests do not assert exact v1 name=cross-restore output.
  • met — Attempt a cross-restore into the existing destination sandbox:: Regression tests invoke runCli(["src", "snapshot", "restore", "--to", "dst"], env) with dst already in fake sandbox list.
  • met — Observe the CLI output and exit code.: Tests capture stdout/stderr and status; the default existing-destination test asserts code 1 and destination-exists output.
  • unknown — Optionally, inspect sbx-dst content (e.g., nemoclaw sbx-dst connect and check filesystem) to confirm that some directories were restored.: No live filesystem inspection is present. The new tests use mocked openshell/docker, and required live E2E for fd2acb2 is not shown passing.

Security review

  • pass — 1. Secrets and Credentials: No hardcoded secrets, API keys, passwords, PEMs, credential JSON, or token literals were added. Test fixtures use fake provider/model/image names.
  • pass — 2. Input Validation and Data Sanitization: Destination sandbox names are validated via validateName(target, 'target sandbox name'). The prior test helper command-injection pattern was addressed by using execFileSync('node', [CLI, ...args]) with an argument array. No unsafe eval/deserialization was introduced.
  • pass — 3. Authentication and Authorization: No new HTTP endpoints or authentication/authorization logic were added. The change operates within the existing local CLI sandbox-management authority model.
  • pass — 4. Dependencies and Third-Party Libraries: No new dependencies or dependency version changes were introduced.
  • pass — 5. Error Handling and Logging: New error messages are targeted and do not expose secrets. The forced delete path fails closed if openshell sandbox delete fails for reasons other than already-gone.
  • pass — 6. Cryptography and Data Protection: Not applicable — no cryptographic operations or sensitive-data encryption changes are introduced.
  • warning — 7. Configuration and Security Headers: No HTTP headers or container image configuration changed, but the PR changes sandbox lifecycle behavior. The forced delete path intentionally skips host-shared cleanup to avoid affecting the source; that is plausible but needs live validation for registry/provider/shields isolation.
  • warning — 8. Security Testing: Regression tests cover refusal, prompt bypass, command-injection-safe test execution, and preflight-before-delete negative cases. However, required live snapshot E2E is not shown passing for the current head SHA, and tests do not fully prove successful destructive delete/recreate/restore behavior.
  • warning — 9. Holistic Security Posture: The default behavior improves safety by refusing silent mutation of an existing sandbox. Remaining concerns are operational: destructive --force delete-and-recreate semantics, TOCTOU-style risk between preflight and delete/recreate, missing current-head E2E, unresolved review thread, and monolith growth in security-sensitive sandbox lifecycle code.

Test / E2E status

  • Test depth: e2e_required — Runtime/sandbox/infrastructure paths need real execution coverage: docs/reference/commands.mdx, src/commands/sandbox/snapshot/restore.ts, src/lib/actions/sandbox/snapshot.ts, src/lib/cli/public-display-defaults.ts. Unit and mocked integration tests are useful but cannot fully prove OpenShell sandbox deletion, recreation, registry cleanup, provider cleanup, shields cleanup, filesystem restore, and policy reconciliation.
  • E2E Advisor: missing
  • Required E2E jobs: snapshot-commands-e2e
  • Missing for analyzed SHA: snapshot-commands-e2e

✅ What looks good

  • Default behavior now refuses cross-sandbox restore into an existing destination, directly addressing the linked silent-mutation/data-loss concern.
  • The implementation validates the snapshot selector and source image before destructive deletion on the forced path.
  • The new tests cover important negative cases: default refusal, bad selector, missing snapshot, unresolvable image, and non-interactive prompt bypass.
  • The CodeRabbit command-injection concern in the test helper was addressed by using execFileSync with an argument array.
  • Docs and CLI display metadata were updated for --force and --yes|-y, and timestamp selector documentation was corrected to exact timestamp behavior.

Review completeness

  • Review is based on trusted metadata, repository read-only inspection, and the provided diff; no scripts, tests, package-manager commands, or workflows were executed.
  • CI and E2E statuses may change after this review; current assessment is for head SHA fd2acb2 only.
  • No live sandbox was created or inspected, so filesystem, OpenShell registry, provider cleanup, and gateway behavior in real environments remain dependent on E2E results.
  • Review-thread state is taken from trusted GitHub metadata; the current file content no longer shows the stripAnsi import, but the thread is still reported unresolved.
  • Human maintainer review required: yes

@github-actions
Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26201035574
Target ref: c31f12033b6c445ccc4498c05c5fba46dc544491
Workflow ref: main
Requested jobs: snapshot-commands-e2e
Summary: 1 passed, 0 failed, 0 skipped

Job Result
snapshot-commands-e2e ✅ success

@github-actions
Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 26201767027
Target ref: fd2acb236ee67ab320577fd633425f4239b6cf14
Workflow ref: main
Requested jobs: snapshot-commands-e2e
Summary: 1 passed, 0 failed, 0 skipped

Job Result
snapshot-commands-e2e ✅ success

@laitingsheng laitingsheng added v0.0.48 Release target v0.0.49 Release target and removed v0.0.47 Release target v0.0.48 Release target labels May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). v0.0.49 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NemoClaw][Linux] Snapshot restore --to incorrectly succeeds when destination sandbox already exists

2 participants