Skip to content

feat(cli): add snapshot create/list/restore commands - #1892

Merged
ericksoa merged 5 commits into
mainfrom
feat/snapshot-commands
Apr 15, 2026
Merged

feat(cli): add snapshot create/list/restore commands#1892
ericksoa merged 5 commits into
mainfrom
feat/snapshot-commands

Conversation

@ericksoa

@ericksoa ericksoa commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Exposes the backup/restore plumbing from sandbox-state.ts (added in #1870) as user-facing CLI commands, per feedback from @bpelfrey:

nemoclaw backup-all — How do you 'restore' this as a user? It almost feels like a place for 'snapshot create' 'snapshot restore' or something.

New commands

nemoclaw <name> snapshot create          Create a timestamped snapshot of sandbox state
nemoclaw <name> snapshot list            List available snapshots with timestamps and versions
nemoclaw <name> snapshot restore [ts]    Restore state from a snapshot (latest if no timestamp given)

Example workflow

# Before a risky operation
$ nemoclaw my-sandbox snapshot create
  ✓ Snapshot created (12 directories)
    ~/.nemoclaw/rebuild-backups/my-sandbox/2026-04-14T...

# Something goes wrong...

# Restore from the snapshot
$ nemoclaw my-sandbox snapshot restore
  Using latest snapshot: 2026-04-14T...
  ✓ Restored 12 directories

Implementation

Thin wrappers around existing functions in src/lib/sandbox-state.ts:

  • backupSandboxState()snapshot create
  • restoreSandboxState()snapshot restore
  • listBackups() / getLatestBackup()snapshot list

No new modules or dependencies — just CLI dispatch wiring.

Test plan

  • All pre-commit and pre-push hooks pass
  • Manual: nemoclaw <name> snapshot create on a live sandbox
  • Manual: nemoclaw <name> snapshot list shows the snapshot
  • Manual: nemoclaw <name> snapshot restore restores state

Closes #1891

Signed-off-by: Aaron Erickson aerickson@nvidia.com

Summary by CodeRabbit

  • New Features

    • Added sandbox snapshot commands: create, list, and restore; list shows timestamp, optional item counts and backup path; restore accepts a timestamp (prefix matches allowed) or defaults to latest and provides clear guidance on ambiguous/missing selections.
  • Tests

    • New end-to-end test validates snapshot create/list/restore flows, targeted restores, and scans backups for sensitive data.
  • Chores

    • CI updated to run snapshot E2E nightly and report failures with artifacts.

Expose the backup/restore plumbing from sandbox-state.ts as user-facing
commands so users can manage sandbox state independently of rebuild:

  nemoclaw <name> snapshot create       Create a timestamped snapshot
  nemoclaw <name> snapshot list         List available snapshots
  nemoclaw <name> snapshot restore [ts] Restore from a snapshot

Closes #1891

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a sandbox-scoped snapshot action with create, list, and restore subcommands to the CLI, implements sandboxSnapshot(sandboxName, subArgs) with live-sandbox checks and detailed reporting, and adds an E2E test plus CI job to validate snapshot behaviors and failure diagnostics.

Changes

Cohort / File(s) Summary
CLI: snapshot command
src/nemoclaw.ts
Adds sandboxSnapshot(sandboxName, subArgs) and routes the "snapshot" action. Implements create (verifies sandbox running via captureOpenshell(["sandbox","list"], {ignoreError:true}), calls sandboxState.backupSandboxState(), reports success with backed-up dir count and path or failure with failed dirs and non‑zero exit), list (calls sandboxState.listBackups() and prints formatted entries: timestamp, optional state-dir count, agent version, path), and restore (selects snapshot by exact timestamp or prefix match or uses getLatestBackup(), errors on ambiguous/missing match, calls sandboxState.restoreSandboxState(), reports partial/failed dirs and exits non‑zero on failure). Updates CLI help and "Valid actions" message to include snapshot.
E2E tests
test/e2e/test-snapshot-commands.sh
New end-to-end script exercising snapshot create, snapshot list, and snapshot restore flows: installs, validates tools, creates markers, captures snapshot paths/timestamps, mutates state, performs latest and timestamped restores and verifies file contents, checks backups for credential-like leaks, prints diagnostics on failure, and destroys the sandbox.
CI workflow
.github/workflows/nightly-e2e.yaml
Adds snapshot-commands-e2e job running test/e2e/test-snapshot-commands.sh with sandbox-specific env vars and artifact upload on failure; updates notify-on-failure job needs and if to include the new job.

Sequence Diagram(s)

sequenceDiagram
    participant User as "User"
    participant CLI as "nemoclaw CLI"
    participant OpenShell as "openshell"
    participant SandboxState as "sandboxState"
    participant FS as "Filesystem/BackupDir"

    User->>CLI: "<sandbox> snapshot create"
    CLI->>OpenShell: "sandbox list" (captureOpenshell)
    OpenShell-->>CLI: running list / error
    CLI->>SandboxState: backupSandboxState(sandbox)
    SandboxState-->>FS: write timestamped backup dir
    SandboxState-->>CLI: success / failure (+ failed dirs)
    CLI-->>User: "Snapshot created" / error

    User->>CLI: "<sandbox> snapshot list"
    CLI->>SandboxState: listBackups(sandbox)
    SandboxState-->>FS: read backup metadata
    SandboxState-->>CLI: list entries
    CLI-->>User: prints snapshot list

    User->>CLI: "<sandbox> snapshot restore [ts]"
    CLI->>SandboxState: resolve backup (exact / prefix / latest)
    SandboxState->>FS: read chosen backup
    SandboxState->>FS: restore files into sandbox
    SandboxState-->>CLI: success / partial / failure (+ failed dirs)
    CLI-->>User: print restore result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped and hid a timestamped treat,
In backup burrows tidy and neat.
Create, list, restore — a carrot parade,
Time-hops saved by the trail I made.
thump 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 accurately summarizes the main change: adding snapshot create/list/restore commands to the CLI.
Linked Issues check ✅ Passed All objectives from issue #1891 are met: snapshot create, list, and restore commands are implemented with proper CLI dispatch wiring calling the existing sandbox-state.ts functions.
Out of Scope Changes check ✅ Passed All changes are in scope: new sandboxSnapshot dispatcher in nemoclaw.ts, E2E workflow job and test script for snapshot commands, no extraneous modifications.

✏️ 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 feat/snapshot-commands

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

🧹 Nitpick comments (1)
src/nemoclaw.ts (1)

1680-1766: Tighten argument validation for snapshot subcommands.

create/list/restore currently accept unexpected trailing args silently. Rejecting extras will make scripting behavior deterministic.

Proposed fix
   switch (subcommand) {
     case "create": {
+      if (subArgs.length > 1) {
+        console.error("  Usage: nemoclaw " + sandboxName + " snapshot create");
+        process.exit(1);
+      }
       const isLive = captureOpenshell(["sandbox", "list"], { ignoreError: true });
@@
     case "list": {
+      if (subArgs.length > 1) {
+        console.error("  Usage: nemoclaw " + sandboxName + " snapshot list");
+        process.exit(1);
+      }
       const backups = sandboxState.listBackups(sandboxName);
@@
     case "restore": {
+      if (subArgs.length > 2) {
+        console.error("  Usage: nemoclaw " + sandboxName + " snapshot restore [timestamp]");
+        process.exit(1);
+      }
       const timestamp = subArgs[1] || null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 1680 - 1766, The snapshot subcommands currently
ignore unexpected trailing arguments; before handling the switch on subcommand
(or at the top of each case), validate subArgs so only allowed argument counts
are accepted: for "create" and "list" require subArgs.length === 1 (no extras),
and for "restore" allow subArgs.length === 1 or 2 (optional timestamp) but
reject >2; on violation print a concise error (e.g., "Unexpected arguments") and
the usage lines and call process.exit(1). Update the logic around the existing
subcommand/subArgs handling (refer to variables subcommand, subArgs and the
"create"/"list"/"restore" case blocks) to perform these checks before executing
backup/restore operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/nemoclaw.ts`:
- Around line 1683-1687: The current logic uses captureOpenshell(["sandbox",
"list"]) and treats any failure the same as the sandbox not running; update the
branch to first check the openshell call result (the captureOpenshell return
value, e.g., isLive.success / isLive.exitCode / isLive.error or absence of
isLive) and if the command failed, log a clear connectivity/gateway error
including the underlying isLive error/output and exit; only when the command
succeeded run parseLiveSandboxNames(isLive.output || "") and then, if liveNames
does not contain sandboxName, log the existing "Sandbox ... is not running"
message and exit.
- Around line 1725-1734: The restore lookup currently uses
b.backupPath.includes(timestamp) which can return multiple partial matches;
update the logic in the sandbox restore block (where sandboxState.listBackups,
sandboxName, timestamp and backupPath are used) to disambiguate: collect all
candidates where b.timestamp === timestamp OR the backup path basename/last
segment equals timestamp (e.g., path.endsWith('/' + timestamp) or compare
path.split('/').pop()), then if zero matches keep the existing error, if more
than one match print an “ambiguous snapshot” error listing the matching
backupPath values and exit, and only set backupPath when exactly one match is
found.

---

Nitpick comments:
In `@src/nemoclaw.ts`:
- Around line 1680-1766: The snapshot subcommands currently ignore unexpected
trailing arguments; before handling the switch on subcommand (or at the top of
each case), validate subArgs so only allowed argument counts are accepted: for
"create" and "list" require subArgs.length === 1 (no extras), and for "restore"
allow subArgs.length === 1 or 2 (optional timestamp) but reject >2; on violation
print a concise error (e.g., "Unexpected arguments") and the usage lines and
call process.exit(1). Update the logic around the existing subcommand/subArgs
handling (refer to variables subcommand, subArgs and the
"create"/"list"/"restore" case blocks) to perform these checks before executing
backup/restore operations.
🪄 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: Pro Plus

Run ID: c1bb41dc-eb5f-404d-85a3-b3c795a492cb

📥 Commits

Reviewing files that changed from the base of the PR and between 8aefa9d and 12519ac.

📒 Files selected for processing (1)
  • src/nemoclaw.ts

Comment thread src/nemoclaw.ts
Comment thread src/nemoclaw.ts
Tests the full snapshot create/list/restore lifecycle:
- Create snapshot, verify it appears in list
- Modify sandbox state, create second snapshot
- Restore latest snapshot, verify state recovered
- Restore first snapshot by timestamp
- Verify no credentials in snapshot directories

Signed-off-by: Aaron Erickson <aerickson@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/e2e/test-snapshot-commands.sh`:
- Around line 129-131: The snapshot timestamp extraction currently allows an
empty SNAPSHOT_TIMESTAMP and continues, making the by-timestamp restore path
optional; change this so that if SNAPSHOT_TIMESTAMP is empty the script fails
immediately with a non-zero exit and clear error (i.e., do not use "|| true"),
and after performing the restore-by-timestamp verify that SECOND_MARKER is
removed (or that restored content matches expected) and if not, print an error
and exit non-zero; update the logic around SNAPSHOT_TIMESTAMP, the restore
invocation that uses it, and the post-restore checks for SECOND_MARKER to
enforce hard failures on parse or restore mismatches (also apply the same strict
behavior in the similar block referenced around lines 170-189).
- Around line 209-214: The help-check currently inspects HELP_OUTPUT for
"snapshot create" and "snapshot restore" but omits "snapshot list", so update
the conditional that checks HELP_OUTPUT (the grep chain around HELP_OUTPUT) to
also verify "snapshot list" is present before calling pass "snapshot help shows
create/list/restore"; ensure the same HELP_OUTPUT variable and success message
are used so the assertion matches the message.
- Around line 144-165: The test currently does a no-op latest restore because
the sandbox already contains the second snapshot; before calling `nemoclaw
"${SANDBOX_NAME}" snapshot restore` mutate the workspace (for example overwrite
or delete the file referenced by `SECOND_MARKER`) so the restore must actually
change state, then run the restore and assert failures on post-restore checks by
replacing the non-failing `info` branch with a hard `fail` when `SECOND_CHECK`
does not equal `SECOND_CONTENT`; reference `RESTORE_OUTPUT`, `SECOND_MARKER`,
`SECOND_CHECK`, and `SECOND_CONTENT` to locate where to inject the perturbation
and change the assertion 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: Pro Plus

Run ID: 070a5367-08ea-44c2-a6f3-0beb44db98b9

📥 Commits

Reviewing files that changed from the base of the PR and between 12519ac and 27a49e6.

📒 Files selected for processing (2)
  • .github/workflows/nightly-e2e.yaml
  • test/e2e/test-snapshot-commands.sh

Comment thread test/e2e/test-snapshot-commands.sh Outdated
Comment thread test/e2e/test-snapshot-commands.sh Outdated
Comment thread test/e2e/test-snapshot-commands.sh

@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

♻️ Duplicate comments (2)
src/nemoclaw.ts (2)

1765-1769: ⚠️ Potential issue | 🟡 Minor

Handle OpenShell query failures before reporting “not running.”

If openshell sandbox list fails here, this branch still falls through to the “not running” message, which is misleading for gateway/runtime connectivity failures. Check isLive.status first and exit with a query failure message before parsing names.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 1765 - 1769, The code currently assumes
captureOpenshell succeeded and reports "not running" incorrectly; before calling
parseLiveSandboxNames, check the captureOpenshell result (isLive.status or
equivalent) and if the query failed log a clear query failure error (including
any isLive.error/output details) and exit, otherwise proceed to
parseLiveSandboxNames and the existing sandboxName check; update the block
around captureOpenshell/parseLiveSandboxNames to bail on query failure instead
of falling through to the "not running" message.

1807-1815: ⚠️ Potential issue | 🟠 Major

Disambiguate snapshot selection before restoring.

backupPath.includes(timestamp) can match multiple snapshots or unrelated path segments, so this can restore the wrong backup. Collect exact candidates, fail on ambiguity, and only continue when there is exactly one match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 1807 - 1815, Replace the single .find(...) with
a filtering step that collects exact candidates (e.g., use
sandboxState.listBackups(sandboxName).filter(...)) rather than using
backupPath.includes(timestamp); then check candidates.length: if 0, print the
existing "No snapshot matching" message and exit; if >1, print an "Ambiguous
snapshot" error listing the matching candidates and exit; only when
candidates.length === 1 set backupPath = candidates[0].backupPath. Keep the
checks using the same variables (timestamp, sandboxName,
sandboxState.listBackups, backupPath) so the logic is localized to this block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/nemoclaw.ts`:
- Around line 1825-1826: Call ensureLiveSandboxOrExit(sandboxName) before
invoking sandboxState.restoreSandboxState to preflight that the target sandbox
is running; modify sandboxSnapshot to be async and await it in the dispatch path
so you can await ensureLiveSandboxOrExit. Specifically, in sandboxSnapshot (the
function that currently calls restoreSandboxState), add an await
ensureLiveSandboxOrExit(sandboxName) immediately prior to the
console.log/restore call, change sandboxSnapshot's signature to async, and
update the code that dispatches/awaits sandboxSnapshot to await the returned
promise. This ensures you validate the sandbox presence before
restoreSandboxState runs.

---

Duplicate comments:
In `@src/nemoclaw.ts`:
- Around line 1765-1769: The code currently assumes captureOpenshell succeeded
and reports "not running" incorrectly; before calling parseLiveSandboxNames,
check the captureOpenshell result (isLive.status or equivalent) and if the query
failed log a clear query failure error (including any isLive.error/output
details) and exit, otherwise proceed to parseLiveSandboxNames and the existing
sandboxName check; update the block around
captureOpenshell/parseLiveSandboxNames to bail on query failure instead of
falling through to the "not running" message.
- Around line 1807-1815: Replace the single .find(...) with a filtering step
that collects exact candidates (e.g., use
sandboxState.listBackups(sandboxName).filter(...)) rather than using
backupPath.includes(timestamp); then check candidates.length: if 0, print the
existing "No snapshot matching" message and exit; if >1, print an "Ambiguous
snapshot" error listing the matching candidates and exit; only when
candidates.length === 1 set backupPath = candidates[0].backupPath. Keep the
checks using the same variables (timestamp, sandboxName,
sandboxState.listBackups, backupPath) so the logic is localized to this block.
🪄 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: Pro Plus

Run ID: cfb0ed04-0dbb-479c-a2c9-4e8eae18fd47

📥 Commits

Reviewing files that changed from the base of the PR and between 27a49e6 and 3bb527c.

📒 Files selected for processing (1)
  • src/nemoclaw.ts

Comment thread src/nemoclaw.ts
Comment on lines +1825 to +1826
console.log(` Restoring snapshot into '${sandboxName}'...`);
const result = sandboxState.restoreSandboxState(sandboxName, backupPath);

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.

⚠️ Potential issue | 🟠 Major

Preflight the target sandbox before restore.

restoreSandboxState() only checks SSH after it finds local state directories. For snapshots with no backed-up dirs, it returns success immediately, so this command can print a successful restore even when '${sandboxName}' is stopped or missing. Call ensureLiveSandboxOrExit(sandboxName) before restoring; that will require making sandboxSnapshot async and awaiting it in the dispatch path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nemoclaw.ts` around lines 1825 - 1826, Call
ensureLiveSandboxOrExit(sandboxName) before invoking
sandboxState.restoreSandboxState to preflight that the target sandbox is
running; modify sandboxSnapshot to be async and await it in the dispatch path so
you can await ensureLiveSandboxOrExit. Specifically, in sandboxSnapshot (the
function that currently calls restoreSandboxState), add an await
ensureLiveSandboxOrExit(sandboxName) immediately prior to the
console.log/restore call, change sandboxSnapshot's signature to async, and
update the code that dispatches/awaits sandboxSnapshot to await the returned
promise. This ensures you validate the sandbox presence before
restoreSandboxState runs.

- Handle openshell query failures separately from "sandbox not running"
- Disambiguate snapshot matching: reject ambiguous partial timestamps
- Fail hard on timestamp parse failure in E2E instead of skipping
- Perturb workspace before latest restore so it's not a no-op
- Assert snapshot list in help check alongside create/restore

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@ericksoa
ericksoa merged commit 2f92692 into main Apr 15, 2026
17 of 21 checks passed
cv pushed a commit that referenced this pull request Apr 15, 2026
## Summary
- Document `nemoclaw <name> snapshot create/list/restore` commands (from
#1892)
- Document `nemoclaw <name> policy-remove` command (from #1822)
- Document `nemoclaw <name> rebuild` command with version staleness
detection (from #1870)
- Document `nemoclaw backup-all` command (from #1870)
- Update `status` to mention live enforced policy display (from #1896)
- Update `connect` and `status` to mention version staleness warnings
(from #1870)
- Update `destroy` to reference snapshots and rebuild as alternatives
- Add snapshot commands section to backup-restore page
- Add `policy-remove` to customize-network-policy page
- Add "Sandbox is running an outdated agent version" troubleshooting
entry
- Bump doc version switcher through 0.0.16
- Regenerate agent skills from updated docs

## Test plan
- [x] `make docs` builds without warnings
- [x] All pre-commit hooks pass
- [ ] Verify rendered pages in docs build output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

## Summary by CodeRabbit

* **Documentation**
* Added documentation for policy preset removal command with selection
flow
* Documented new rebuild command for sandbox upgrades while preserving
workspace state
* Introduced snapshot commands for creating, listing, and restoring
workspace backups
  * Added bulk backup capability for multiple sandboxes
* Enhanced agent version warnings in connect and status commands with
remediation guidance
* Expanded troubleshooting guide with outdated agent version resolution
steps

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wscurran wscurran added the VDR Linked to VDR finding label May 11, 2026
@wscurran wscurran added the feature PR adds or expands user-visible functionality label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature PR adds or expands user-visible functionality VDR Linked to VDR finding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): add snapshot create/restore/list commands for user-accessible state management

3 participants