Skip to content

feat(blueprint): add snapshot prune/delete commands for retention management#5453

Open
vasanth53 wants to merge 6 commits into
NVIDIA:mainfrom
vasanth53:feat/snapshot-retention-management
Open

feat(blueprint): add snapshot prune/delete commands for retention management#5453
vasanth53 wants to merge 6 commits into
NVIDIA:mainfrom
vasanth53:feat/snapshot-retention-management

Conversation

@vasanth53

@vasanth53 vasanth53 commented Jun 15, 2026

Copy link
Copy Markdown

Summary

Adds snapshots list, snapshots prune --keep <N>, and snapshots delete --path <path> subcommands to the blueprint runner, letting users manage disk space consumed by accumulated migration snapshots under ~/.nemoclaw/snapshots/.

Changes

File Change
nemoclaw/src/blueprint/snapshot.ts Added deleteSnapshot(), pruneSnapshots() with symlink-attack rejection
nemoclaw/src/blueprint/snapshot.test.ts 8 tests covering deletion, symlink rejection, prune with keep count, empty state
nemoclaw/src/blueprint/runner.ts Added snapshots action with list, prune, delete subcommands and usage text
nemoclaw/src/blueprint/runner.test.ts 12 tests for CLI routing, list/prune/delete flows; extended fs mock with lstatSync, readlinkSync, rmSync
ci/test-file-size-budget.json Ratcheted runner.test.ts size budget to 1622 lines

Usage

nemoclaw blueprint snapshots list
nemoclaw blueprint snapshots prune --keep 5
nemoclaw blueprint snapshots delete --path ~/.nemoclaw/snapshots/20260101T000000Z

Testing

All 503 plugin tests pass. All hooks pass (pre-commit, commitlint). New code adds 20 tests across two test files.

Checklist

  • Conventional Commit (feat(blueprint):)
  • DCO sign-off
  • Tests added/updated
  • All tests pass
  • Hooks pass (pre-commit, commitlint)
  • Feature branch from main

Closes #5452

Summary by CodeRabbit

  • New Features
    • Added snapshot management commands to the CLI
    • List all snapshots with metadata (timestamp, path, source, file count)
    • Prune snapshots by specifying the number to retain
    • Delete individual snapshots by path
    • All commands include help text and comprehensive error handling

@copy-pr-bot

copy-pr-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Two new exported functions, deleteSnapshot and pruneSnapshots, are added to snapshot.ts. The CLI runner gains a snapshots top-level action with list, prune --keep <N>, and delete --path <path> subcommands dispatched via actionSnapshots. Tests cover deletion, symlink rejection, prune edge cases, and CLI routing.

Changes

Snapshot Retention Management

Layer / File(s) Summary
deleteSnapshot and pruneSnapshots utilities
nemoclaw/src/blueprint/snapshot.ts, nemoclaw/src/blueprint/snapshot.test.ts
deleteSnapshot calls rejectSymlinksOnPath then rmSync recursively, returning a boolean. pruneSnapshots(keep) calls listSnapshots(), deletes entries beyond the first keep, and returns { deleted, kept, failed }. Tests cover successful deletion, symlink rejection, non-existent paths, keep-larger-than-count no-op, empty snapshot list, and symlink-caused failure during prune.
CLI runner snapshots action and tests
nemoclaw/src/blueprint/runner.ts, nemoclaw/src/blueprint/snapshot.test.ts
Imports snapshot helpers, adds SnapshotsAction union type (list | prune | delete) and isSnapshotsAction type guard. Updates main to dispatch snapshots to actionSnapshots. Implements snapshotsUsage() and actionSnapshots(argv) with --help, --keep integer parsing, --path argument parsing, and logging for each subcommand. Extends CLI dispatch tests for prune output and delete-by-path cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Snapshots piling up, oh my!
With pruneSnapshots the old ones die.
deleteSnapshot guards each path,
No symlink tricks shall cross its wrath.
The CLI now lists, prunes, and clears —
A tidy ~/.nemoclaw appears! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% 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 describes the main changes: adding snapshot prune/delete commands for retention management, which aligns with the PR's core functionality.
Linked Issues check ✅ Passed All linked issue requirements are implemented: snapshots list displays snapshot metadata, prune --keep retains N recent snapshots, delete --path removes by path, and symlink rejection via rejectSymlinksOnPath is consistently applied.
Out of Scope Changes check ✅ Passed All changes align with issue #5452 requirements. The implementation focuses on snapshot management commands and their supporting functions, with test coverage and CI budget updates that directly support the feature.

✏️ 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: 1

🤖 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 `@nemoclaw/src/blueprint/snapshot.ts`:
- Around line 329-346: The pruneSnapshots function silently excludes snapshot
paths when deleteSnapshot returns false, leaving them out of both the deleted
and kept arrays, which hides failures from users. Add a failed array to track
snapshot paths where deleteSnapshot returns false, and include it in the
returned object alongside deleted and kept. This gives users visibility into
which specific snapshots failed to delete and why the counts might not add up to
the total.
🪄 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: c471987d-87b6-4b91-bb33-a58a59186f72

📥 Commits

Reviewing files that changed from the base of the PR and between f4f3c58 and 66dc1d2.

📒 Files selected for processing (5)
  • ci/test-file-size-budget.json
  • nemoclaw/src/blueprint/runner.test.ts
  • nemoclaw/src/blueprint/runner.ts
  • nemoclaw/src/blueprint/snapshot.test.ts
  • nemoclaw/src/blueprint/snapshot.ts

Comment thread nemoclaw/src/blueprint/snapshot.ts Outdated
@vasanth53 vasanth53 force-pushed the feat/snapshot-retention-management branch from 66dc1d2 to 337c93b Compare June 15, 2026 06:31

@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 (1)
nemoclaw/src/blueprint/snapshot.ts (1)

319-327: ⚡ Quick win

Consider validating that snapshotPath is within SNAPSHOTS_DIR.

rejectSymlinksOnPath guards against symlink attacks under HOME but doesn't restrict deletion to the snapshots directory. A user could inadvertently (or maliciously via a compromised script) run snapshots delete --path ~/.ssh and delete unrelated directories.

Adding a prefix check provides defense-in-depth:

🛡️ Suggested validation
 export function deleteSnapshot(snapshotPath: string): boolean {
   try {
+    const resolved = resolve(snapshotPath);
+    if (!resolved.startsWith(SNAPSHOTS_DIR + '/') && resolved !== SNAPSHOTS_DIR) {
+      return false;
+    }
     rejectSymlinksOnPath(snapshotPath);
     rmSync(snapshotPath, { recursive: true, force: true });
     return true;
   } catch {
     return false;
   }
 }
🤖 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 `@nemoclaw/src/blueprint/snapshot.ts` around lines 319 - 327, The
deleteSnapshot function currently only guards against symlinks but doesn't
restrict path deletion to the SNAPSHOTS_DIR directory, allowing arbitrary
directory deletion. Add a validation check in deleteSnapshot that ensures
snapshotPath is within SNAPSHOTS_DIR before proceeding with rejectSymlinksOnPath
and rmSync calls. If the path is outside SNAPSHOTS_DIR, return false to prevent
the deletion.
🤖 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 `@nemoclaw/src/blueprint/snapshot.ts`:
- Around line 329-333: The pruneSnapshots function does not validate the keep
parameter for negative values, and when keep is negative, the slice(keep)
operation will behave unexpectedly by returning elements from the end of the
array. Add a defensive guard at the beginning of the pruneSnapshots function to
either throw an error for invalid negative values or clamp the keep value to a
minimum of 0 to ensure predictable behavior.

---

Nitpick comments:
In `@nemoclaw/src/blueprint/snapshot.ts`:
- Around line 319-327: The deleteSnapshot function currently only guards against
symlinks but doesn't restrict path deletion to the SNAPSHOTS_DIR directory,
allowing arbitrary directory deletion. Add a validation check in deleteSnapshot
that ensures snapshotPath is within SNAPSHOTS_DIR before proceeding with
rejectSymlinksOnPath and rmSync calls. If the path is outside SNAPSHOTS_DIR,
return false to prevent the deletion.
🪄 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: 4406e8b8-b940-40ca-bc6a-54f757bce558

📥 Commits

Reviewing files that changed from the base of the PR and between 66dc1d2 and 337c93b.

📒 Files selected for processing (5)
  • ci/test-file-size-budget.json
  • nemoclaw/src/blueprint/runner.test.ts
  • nemoclaw/src/blueprint/runner.ts
  • nemoclaw/src/blueprint/snapshot.test.ts
  • nemoclaw/src/blueprint/snapshot.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • ci/test-file-size-budget.json
  • nemoclaw/src/blueprint/snapshot.test.ts
  • nemoclaw/src/blueprint/runner.test.ts
  • nemoclaw/src/blueprint/runner.ts

Comment on lines +329 to +333
export function pruneSnapshots(keep: number): { deleted: string[]; kept: string[]; failed: string[] } {
const snapshots = listSnapshots();
if (snapshots.length <= keep) {
return { deleted: [], kept: snapshots.map((s) => s.path), failed: [] };
}

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 | 🟡 Minor | ⚡ Quick win

Handle negative keep values defensively.

If keep is negative, slice(keep) returns elements from the end of the array, producing unexpected behavior (e.g., pruneSnapshots(-1) would keep all but the oldest snapshot instead of rejecting the input).

Consider clamping or throwing early:

🛡️ Suggested guard
 export function pruneSnapshots(keep: number): { deleted: string[]; kept: string[]; failed: string[] } {
+  if (keep < 0) {
+    throw new Error(`Invalid keep count: ${keep}. Must be a non-negative integer.`);
+  }
   const snapshots = listSnapshots();
   if (snapshots.length <= keep) {
🤖 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 `@nemoclaw/src/blueprint/snapshot.ts` around lines 329 - 333, The
pruneSnapshots function does not validate the keep parameter for negative
values, and when keep is negative, the slice(keep) operation will behave
unexpectedly by returning elements from the end of the array. Add a defensive
guard at the beginning of the pruneSnapshots function to either throw an error
for invalid negative values or clamp the keep value to a minimum of 0 to ensure
predictable behavior.

…agement

Add deleteSnapshot and pruneSnapshots to the blueprint snapshot module so
users can clean up accumulated migration snapshots under ~/.nemoclaw/snapshots/.

- deleteSnapshot(path): remove a single snapshot directory, rejecting symlinks
- pruneSnapshots(keep): keep N most recent, delete the rest, report failures
- Wire through CLI as 'snapshots list', 'snapshots prune --keep N',
  and 'snapshots delete --path <path>' subcommands
- Test CLI dispatch in snapshot.test.ts to stay under test file size budget
- 20 new tests covering snapshot CLI and edge cases

Signed-off-by: vasanth53 <vasanth@peak42.in>
@vasanth53 vasanth53 force-pushed the feat/snapshot-retention-management branch from 337c93b to 5e76755 Compare June 15, 2026 06:37

@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 `@nemoclaw/src/blueprint/runner.ts`:
- Around line 1092-1104: The delete command accepts a user-provided --path
argument that is passed directly to deleteSnapshot without validation, allowing
deletion of arbitrary directories outside the snapshots directory. After
extracting snapshotPath from argv (around line 1095), add validation to ensure
the path is constrained to the snapshots directory by resolving it to an
absolute path and verifying it starts with the expected snapshots directory path
(typically ~/.nemoclaw/snapshots). Reject the command with an appropriate error
message if the resolved path attempts to escape the snapshots directory. This
validation must occur before the deleteSnapshot function is called to prevent
unauthorized recursive deletion of directories outside the snapshots location.
- Around line 1065-1067: The `Number.parseInt` function at the keep variable
assignment performs lenient parsing and will accept malformed inputs like "3abc"
or "1.5" by silently truncating them to 3 and 1 respectively. To enforce the
"non-negative integer" requirement strictly, validate that the entire input
string represents a pure integer with no trailing characters or decimal points.
You can do this by checking that the parsed keep value, when converted back to a
string, matches the original trimmed input value, or by validating the input
format using a regex pattern that matches only optional whitespace and
non-negative integer digits before attempting to parse.
🪄 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: bd9bfb48-d9d7-47f7-8974-9fc2e082f74f

📥 Commits

Reviewing files that changed from the base of the PR and between 337c93b and 5e76755.

📒 Files selected for processing (3)
  • nemoclaw/src/blueprint/runner.ts
  • nemoclaw/src/blueprint/snapshot.test.ts
  • nemoclaw/src/blueprint/snapshot.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • nemoclaw/src/blueprint/snapshot.ts
  • nemoclaw/src/blueprint/snapshot.test.ts

Comment on lines +1065 to +1067
keep = Number.parseInt(val, 10);
if (!Number.isFinite(keep) || keep < 0) {
throw new Error("--keep must be a non-negative integer");

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 | 🟡 Minor | ⚡ Quick win

--keep validation accepts malformed values.

Line 1065 uses Number.parseInt, so inputs like --keep 3abc or --keep 1.5 are accepted as 3 / 1, despite the “non-negative integer” requirement.

Suggested fix
-          keep = Number.parseInt(val, 10);
-          if (!Number.isFinite(keep) || keep < 0) {
+          if (!/^\d+$/.test(val)) {
+            throw new Error("--keep must be a non-negative integer");
+          }
+          keep = Number.parseInt(val, 10);
+          if (!Number.isSafeInteger(keep) || keep < 0) {
             throw new Error("--keep must be a non-negative integer");
           }
🤖 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 `@nemoclaw/src/blueprint/runner.ts` around lines 1065 - 1067, The
`Number.parseInt` function at the keep variable assignment performs lenient
parsing and will accept malformed inputs like "3abc" or "1.5" by silently
truncating them to 3 and 1 respectively. To enforce the "non-negative integer"
requirement strictly, validate that the entire input string represents a pure
integer with no trailing characters or decimal points. You can do this by
checking that the parsed keep value, when converted back to a string, matches
the original trimmed input value, or by validating the input format using a
regex pattern that matches only optional whitespace and non-negative integer
digits before attempting to parse.

Comment on lines +1092 to +1104
let snapshotPath: string | undefined;
for (let i = 1; i < argv.length; i++) {
if (argv[i] === "--path") {
snapshotPath = argv[++i];
}
}
if (!snapshotPath) throw new Error("--path is required for delete");

if (deleteSnapshot(snapshotPath)) {
log(`Deleted snapshot: ${snapshotPath}`);
} else {
throw new Error(`Failed to delete snapshot: ${snapshotPath}`);
}

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 | ⚡ Quick win

delete --path allows recursive deletion outside snapshots directory.

Lines 1092-1104 pass user-provided --path straight into recursive deletion. Without constraining it to ~/.nemoclaw/snapshots, this can remove arbitrary directories the user has permissions for.

Suggested fix
-import { join, sep } from "node:path";
+import { join, resolve, sep } from "node:path";
...
     case "delete": {
       let snapshotPath: string | undefined;
       for (let i = 1; i < argv.length; i++) {
         if (argv[i] === "--path") {
           snapshotPath = argv[++i];
         }
       }
       if (!snapshotPath) throw new Error("--path is required for delete");
+
+      const snapshotsRoot = resolve(join(homedir(), ".nemoclaw", "snapshots"));
+      const targetPath = resolve(snapshotPath);
+      if (!targetPath.startsWith(snapshotsRoot + sep)) {
+        throw new Error(`--path must be under ${snapshotsRoot}`);
+      }

-      if (deleteSnapshot(snapshotPath)) {
-        log(`Deleted snapshot: ${snapshotPath}`);
+      if (deleteSnapshot(targetPath)) {
+        log(`Deleted snapshot: ${targetPath}`);
       } else {
-        throw new Error(`Failed to delete snapshot: ${snapshotPath}`);
+        throw new Error(`Failed to delete snapshot: ${targetPath}`);
       }
       break;
     }
🤖 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 `@nemoclaw/src/blueprint/runner.ts` around lines 1092 - 1104, The delete
command accepts a user-provided --path argument that is passed directly to
deleteSnapshot without validation, allowing deletion of arbitrary directories
outside the snapshots directory. After extracting snapshotPath from argv (around
line 1095), add validation to ensure the path is constrained to the snapshots
directory by resolving it to an absolute path and verifying it starts with the
expected snapshots directory path (typically ~/.nemoclaw/snapshots). Reject the
command with an appropriate error message if the resolved path attempts to
escape the snapshots directory. This validation must occur before the
deleteSnapshot function is called to prevent unauthorized recursive deletion of
directories outside the snapshots location.

@wscurran wscurran added area: cli Command line interface, flags, terminal UX, or output feature PR adds or expands user-visible functionality labels Jun 23, 2026
@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for adding the snapshots list, snapshots prune --keep <N>, and snapshots delete --path <path> subcommands to the blueprint runner. This proposes a way to manage disk space consumed by accumulated migration snapshots under ~/.nemoclaw/snapshots/ through new CLI commands.


Related open issues:

1 similar comment
@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for adding the snapshots list, snapshots prune --keep <N>, and snapshots delete --path <path> subcommands to the blueprint runner. This proposes a way to manage disk space consumed by accumulated migration snapshots under ~/.nemoclaw/snapshots/ through new CLI commands.


Related open issues:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Command line interface, flags, terminal UX, or output feature PR adds or expands user-visible functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(blueprint): add snapshot prune/delete commands for retention management

2 participants