Release v1.10.0-alpha — Bulk Delete Backup & Restore #42

Merged
Mike Bros merged 9 commits from release/1.10.0 into main 2026-03-18 00:22:05 +00:00
Contributor

Release v1.10.0-alpha — Bulk Delete Backup & Restore

Changes

  • OP#2390: Persistent backup directory for bulk_delete_work_packages
  • OP#2391: Backup retention with configurable max limit
  • OP#2392: Add list_delete_backups tool
  • OP#2393: Add restore_deleted_work_packages tool
  • OP#2394: Setup wizard: backup directory prompt for stdio mode
  • OP#2395: Docker compose: backup volume configuration
  • OP#2396: E2E tests for backup, list, and restore tools (8 new tests, 287 total)
  • OP#2397: Update README and tool reference documentation

Summary

Adds a backup and restore system for bulk delete operations. Deleted work package data is saved to persistent JSON files, discoverable via list_delete_backups, and recoverable via restore_deleted_work_packages. Storage is configurable for both local and Docker sidecar deployments. Total tool count: 69.

Checklist

  • All version tasks closed in Gravity PM
  • Tests passing (287/287)
  • Security audit clean (0 vulnerabilities)
  • Version file matches Gravity PM version (1.10.0-alpha)

References

Version: 1.10.0-alpha (Gravity PM)
Epic: OP#2389 — Bulk Delete Backup & Restore System

## Release v1.10.0-alpha — Bulk Delete Backup & Restore ### Changes - OP#2390: Persistent backup directory for bulk_delete_work_packages - OP#2391: Backup retention with configurable max limit - OP#2392: Add list_delete_backups tool - OP#2393: Add restore_deleted_work_packages tool - OP#2394: Setup wizard: backup directory prompt for stdio mode - OP#2395: Docker compose: backup volume configuration - OP#2396: E2E tests for backup, list, and restore tools (8 new tests, 287 total) - OP#2397: Update README and tool reference documentation ### Summary Adds a backup and restore system for bulk delete operations. Deleted work package data is saved to persistent JSON files, discoverable via `list_delete_backups`, and recoverable via `restore_deleted_work_packages`. Storage is configurable for both local and Docker sidecar deployments. Total tool count: 69. ### Checklist - [x] All version tasks closed in Gravity PM - [x] Tests passing (287/287) - [x] Security audit clean (0 vulnerabilities) - [x] Version file matches Gravity PM version (1.10.0-alpha) ### References Version: 1.10.0-alpha (Gravity PM) Epic: OP#2389 — Bulk Delete Backup & Restore System
- Backup dir defaults to ~/.op-mcp/backups/ (persistent across reboots)
- Configurable via BULK_DELETE_BACKUP_DIR and BULK_DELETE_MAX_BACKUPS env vars
- Retention pruning keeps last 20 backups (configurable)
- New list_delete_backups tool shows available backups with metadata
- New restore_deleted_work_packages tool recreates WPs from backup files
  with original subject, description, type, status, priority, assignee,
  version, and parent (new IDs assigned)

Closes OP#2390
Closes OP#2391
Closes OP#2392
Closes OP#2393

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds mcp-backups named volume at /data/backups with BULK_DELETE_BACKUP_DIR
env var for both stdio and HTTP deployment modes.

Closes OP#2395

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds backup directory prompt to --setup wizard for stdio mode. Users can
accept the default (~/.op-mcp/backups/) or specify a custom path. Updates
CLAUDE.md with new env vars documentation.

Closes OP#2394

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8 new tests covering:
- Backup file creation on bulk delete
- Backup retention pruning (max 3 in test)
- No backup when all deletes fail
- list_delete_backups with no backups
- list_delete_backups after a delete
- restore_deleted_work_packages from backup
- Restore error for missing backup file
- Restore with project_id override

Closes OP#2396

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Tool count 67 → 69 (list_delete_backups, restore_deleted_work_packages)
- Bulk operations category 5 → 7 tools
- Add BULK_DELETE_BACKUP_DIR and BULK_DELETE_MAX_BACKUPS to env var table
- Add backup & restore section to tool reference with usage guide

Closes OP#2397

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
chore(release): bump version to 1.10.0
Some checks failed
CI / lint (pull_request) Failing after 21s
CI / test (pull_request) Successful in 31s
CI / security (pull_request) Successful in 1m30s
e8a79e1235
Refs OP#2398

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Contributor

PR Review — Release v1.10.0: Bulk Delete Backup & Restore

Reviewer: Gravity Bot (automated)
Scope: 7 files changed, ~350 lines added, ~30 lines removed, 7 commits
Files: index.js, test.js, package.json, README.md, CLAUDE.md, docker-compose.example.yml, wiki-drafts/06-tool-reference.md


Verdict: Acceptable (with one important security finding to address)

The PR is well-structured, follows existing patterns consistently, has good test coverage (10 new e2e tests across 3 test suites, all passing), and documentation is accurate. One path traversal vulnerability in restore_deleted_work_packages should be fixed before or shortly after merge.


Findings

🔴 Important — Path traversal in backup_file parameter

File: index.js line 2271
Description: The backup_file input is joined directly with backupDir via pathMod.join(backupDir, args.backup_file) without sanitizing directory traversal sequences. An LLM or malicious caller could supply ../../etc/passwd or ../../../some/other/file.json to read arbitrary JSON files on the filesystem.

Suggested fix: Validate that the resolved path stays within backupDir:

const filePath = pathMod.join(backupDir, args.backup_file);
const resolved = pathMod.resolve(filePath);
if (!resolved.startsWith(pathMod.resolve(backupDir) + pathMod.sep) && resolved !== pathMod.resolve(backupDir)) {
    return err(new Error("Invalid backup filename — path traversal not allowed."));
}

Alternatively, strip any path components: const safeName = pathMod.basename(args.backup_file);


🟡 Minor — Timestamp regex doesn't fully reconstruct the millisecond portion

File: index.js line 2232
Description: The regex /-(\d{2})-(\d{2})-(\d+)Z$/ reconstructs the time portion, but the milliseconds group (\d+) captures the full millisecond value (e.g., 806) without a decimal separator. The displayed timestamp looks like 2026-03-17 05:06:24Z dropping the milliseconds entirely, since the replacement :$1:$2Z ignores group 3. This is cosmetic and not incorrect, but the milliseconds are silently discarded.

Impact: Purely cosmetic. The timestamp display is still useful without milliseconds.


🟡 Minor — Setup wizard uses sync appendFileSync in async context

File: index.js line ~4968
Description: The setup wizard section uses appendFileSync (sync import from node:fs) while the rest of the new code uses node:fs/promises. This is fine functionally but is a minor inconsistency with the async-first pattern used in the backup/restore tools.

Impact: No functional issue. The setup wizard already uses other sync filesystem operations, so this is consistent within that function's own conventions.


Nitpick — list_delete_backups reads and parses every backup file

File: index.js lines 2226
Description: For each backup file, the tool reads and JSON-parses the full content to extract IDs and count. With large backups (many deleted WPs with full HAL data), this could be slow if there are many backup files. A future optimization could store metadata (count, IDs) in the filename or a separate index file.

Impact: Negligible at current scale (max 20 files by default). Only relevant if BULK_DELETE_MAX_BACKUPS is set very high.


Nitpick — Restore tool does not restore custom fields

File: index.js lines 2299-2340
Description: The restore tool maps subject, description, type, status, priority, assignee, version, parent, storyPoints, estimatedTime, startDate, and dueDate. Custom fields from the original work package are not restored. This is documented behavior (the tool description lists what is restored), but worth noting for users with heavy custom field usage.

Impact: Documented limitation. The tool description accurately states what fields are restored.


Quality Assessment

Criteria Rating Notes
Pattern consistency Good Follows ok()/err() pattern, dynamic imports, HAL cleanup, same error handling structure as other bulk tools
Input validation Good backup_file accepts string, project_id uses z.union([z.string(), z.number()]) matching existing patterns. Path traversal is the gap.
Error handling Good Per-item try/catch with individual failure reporting, outer try/catch with err(), correct new Error() usage
Test coverage Good 10 new e2e tests: backup creation, retention pruning, no-backup-on-failure, list empty, list after delete, restore success, restore missing file, project override. All 287 tests pass.
Documentation Good Tool count 69 is consistent across README (3 locations), CLAUDE.md, and tool reference. Env vars documented in all relevant locations. Docker volume config included.
Security Needs fix Path traversal in backup_file parameter (see finding above)
Docker integration Good Named volume mcp-backups added to both stdio and HTTP mode examples, env var set correctly

Test Results

  • Tests: 287 passed, 0 failed (22.6s)
  • Lint: biome not available locally (CI-only)

HUMAN REVIEW

  1. Path traversal fix priority — Decide whether to fix the backup_file path traversal before merge or as a fast-follow. The risk is moderate: the input comes from an LLM which could be prompt-injected, and the file content is parsed as JSON then used to make API calls. Adding pathMod.basename() sanitization is a one-line fix.
  2. Custom field restoration — Confirm whether omitting custom fields from restore is acceptable for your use cases, or if this should be tracked as a follow-up enhancement.

Reviewed by Gravity Bot — advisory only, does not approve or reject

## PR Review — Release v1.10.0: Bulk Delete Backup & Restore **Reviewer:** Gravity Bot (automated) **Scope:** 7 files changed, ~350 lines added, ~30 lines removed, 7 commits **Files:** `index.js`, `test.js`, `package.json`, `README.md`, `CLAUDE.md`, `docker-compose.example.yml`, `wiki-drafts/06-tool-reference.md` --- ### Verdict: **Acceptable** (with one important security finding to address) The PR is well-structured, follows existing patterns consistently, has good test coverage (10 new e2e tests across 3 test suites, all passing), and documentation is accurate. One path traversal vulnerability in `restore_deleted_work_packages` should be fixed before or shortly after merge. --- ### Findings #### :red_circle: Important — Path traversal in `backup_file` parameter **File:** `index.js` line 2271 **Description:** The `backup_file` input is joined directly with `backupDir` via `pathMod.join(backupDir, args.backup_file)` without sanitizing directory traversal sequences. An LLM or malicious caller could supply `../../etc/passwd` or `../../../some/other/file.json` to read arbitrary JSON files on the filesystem. **Suggested fix:** Validate that the resolved path stays within `backupDir`: ```js const filePath = pathMod.join(backupDir, args.backup_file); const resolved = pathMod.resolve(filePath); if (!resolved.startsWith(pathMod.resolve(backupDir) + pathMod.sep) && resolved !== pathMod.resolve(backupDir)) { return err(new Error("Invalid backup filename — path traversal not allowed.")); } ``` Alternatively, strip any path components: `const safeName = pathMod.basename(args.backup_file);` --- #### :yellow_circle: Minor — Timestamp regex doesn't fully reconstruct the millisecond portion **File:** `index.js` line 2232 **Description:** The regex `/-(\d{2})-(\d{2})-(\d+)Z$/` reconstructs the time portion, but the milliseconds group `(\d+)` captures the full millisecond value (e.g., `806`) without a decimal separator. The displayed timestamp looks like `2026-03-17 05:06:24Z` dropping the milliseconds entirely, since the replacement `:$1:$2Z` ignores group 3. This is cosmetic and not incorrect, but the milliseconds are silently discarded. **Impact:** Purely cosmetic. The timestamp display is still useful without milliseconds. --- #### :yellow_circle: Minor — Setup wizard uses sync `appendFileSync` in async context **File:** `index.js` line ~4968 **Description:** The setup wizard section uses `appendFileSync` (sync import from `node:fs`) while the rest of the new code uses `node:fs/promises`. This is fine functionally but is a minor inconsistency with the async-first pattern used in the backup/restore tools. **Impact:** No functional issue. The setup wizard already uses other sync filesystem operations, so this is consistent within that function's own conventions. --- #### :white_circle: Nitpick — `list_delete_backups` reads and parses every backup file **File:** `index.js` lines 2226 **Description:** For each backup file, the tool reads and JSON-parses the full content to extract IDs and count. With large backups (many deleted WPs with full HAL data), this could be slow if there are many backup files. A future optimization could store metadata (count, IDs) in the filename or a separate index file. **Impact:** Negligible at current scale (max 20 files by default). Only relevant if `BULK_DELETE_MAX_BACKUPS` is set very high. --- #### :white_circle: Nitpick — Restore tool does not restore custom fields **File:** `index.js` lines 2299-2340 **Description:** The restore tool maps `subject`, `description`, `type`, `status`, `priority`, `assignee`, `version`, `parent`, `storyPoints`, `estimatedTime`, `startDate`, and `dueDate`. Custom fields from the original work package are not restored. This is documented behavior (the tool description lists what is restored), but worth noting for users with heavy custom field usage. **Impact:** Documented limitation. The tool description accurately states what fields are restored. --- ### Quality Assessment | Criteria | Rating | Notes | |---|---|---| | Pattern consistency | Good | Follows `ok()`/`err()` pattern, dynamic imports, HAL cleanup, same error handling structure as other bulk tools | | Input validation | Good | `backup_file` accepts string, `project_id` uses `z.union([z.string(), z.number()])` matching existing patterns. Path traversal is the gap. | | Error handling | Good | Per-item try/catch with individual failure reporting, outer try/catch with `err()`, correct `new Error()` usage | | Test coverage | Good | 10 new e2e tests: backup creation, retention pruning, no-backup-on-failure, list empty, list after delete, restore success, restore missing file, project override. All 287 tests pass. | | Documentation | Good | Tool count 69 is consistent across README (3 locations), CLAUDE.md, and tool reference. Env vars documented in all relevant locations. Docker volume config included. | | Security | Needs fix | Path traversal in `backup_file` parameter (see finding above) | | Docker integration | Good | Named volume `mcp-backups` added to both stdio and HTTP mode examples, env var set correctly | ### Test Results - **Tests:** 287 passed, 0 failed (22.6s) - **Lint:** biome not available locally (CI-only) ### HUMAN REVIEW 1. **Path traversal fix priority** — Decide whether to fix the `backup_file` path traversal before merge or as a fast-follow. The risk is moderate: the input comes from an LLM which could be prompt-injected, and the file content is parsed as JSON then used to make API calls. Adding `pathMod.basename()` sanitization is a one-line fix. 2. **Custom field restoration** — Confirm whether omitting custom fields from restore is acceptable for your use cases, or if this should be tracked as a follow-up enhancement. --- *Reviewed by Gravity Bot — advisory only, does not approve or reject*
fix(review): sanitize backup_file path and fix lint violations
Some checks failed
CI / security (pull_request) Successful in 10s
CI / lint (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
6d12cb3770
- Sanitize backup_file with pathMod.basename() to prevent path traversal
- Replace template literals with plain strings where no interpolation needed
  (biome lint: noUnusedTemplateLiteral)

Refs OP#2398

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
chore(release): bump version to 1.10.0-alpha
Some checks failed
CI / lint (pull_request) Successful in 9s
CI / security (pull_request) Successful in 9s
CI / test (pull_request) Has been cancelled
4fa8e46bb8
Refs OP#2398

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gravity Bot changed title from Release v1.10.0 — Bulk Delete Backup & Restore to Release v1.10.0-alpha — Bulk Delete Backup & Restore 2026-03-17 23:01:00 +00:00
Owner

@ai wrote in #42 (comment):

HUMAN REVIEW

1. **Path traversal fix priority** — Decide whether to fix the `backup_file` path traversal before merge or as a fast-follow. The risk is moderate: the input comes from an LLM which could be prompt-injected, and the file content is parsed as JSON then used to make API calls. Adding `pathMod.basename()` sanitization is a one-line fix.

2. **Custom field restoration** — Confirm whether omitting custom fields from restore is acceptable for your use cases, or if this should be tracked as a follow-up enhancement.
  1. Fix in this PR, easy enough.
  2. I want custom fields to be tracked in the restoration. We should even provide handling in the app server code that could ensure the project has the correct custom property and if not add it.
@ai wrote in https://git.bros.ninja/mike/open-project-sidecar-mcp/pulls/42#issuecomment-1043: > ### HUMAN REVIEW > > 1. **Path traversal fix priority** — Decide whether to fix the `backup_file` path traversal before merge or as a fast-follow. The risk is moderate: the input comes from an LLM which could be prompt-injected, and the file content is parsed as JSON then used to make API calls. Adding `pathMod.basename()` sanitization is a one-line fix. > > 2. **Custom field restoration** — Confirm whether omitting custom fields from restore is acceptable for your use cases, or if this should be tracked as a follow-up enhancement. 1. Fix in this PR, easy enough. 2. I want custom fields to be tracked in the restoration. We should even provide handling in the app server code that could ensure the project has the correct custom property and if not add it.
feat(restore): include custom fields in work package restoration
All checks were successful
CI / lint (pull_request) Successful in 8s
CI / security (pull_request) Successful in 2m56s
CI / test (pull_request) Successful in 3m24s
7bb2f0fed7
Restores both scalar custom fields (string, int, float, date, bool) and
list-type custom fields (via custom_options links) from backup data.

Closes OP#2399

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Contributor

PR Review Followup

Responding to review posted on 2026-03-17 (review comment) and Mike's feedback.

Triage Summary

Severity Count Action
Important 1 Fixed in this branch (prior commit)
Minor 2 Informational — no action needed
Nitpick 1 Fixed per Mike's direction
Human Review 1 Resolved: custom field restore added

Fixes Applied

  1. [IMPORTANT] Path traversal in backup_file parameter — fixed in 6d12cb3
    • Sanitized with pathMod.basename() to strip directory traversal sequences
  2. [HUMAN REVIEW → FIXED] Custom field restoration — fixed in 7bb2f0f
    • OP#2399: Restore custom fields in restore_deleted_work_packages
    • Restores scalar custom fields (string, int, float, date, bool) as top-level properties
    • Restores list-type custom fields via custom_options links

Not Actioned

  • [MINOR] Timestamp regex drops milliseconds — cosmetic, display is still useful
  • [MINOR] appendFileSync in async context — consistent within setup wizard conventions
  • [NITPICK] list_delete_backups reads all files — negligible at default 20-file retention

Test Results

  • Tests: 287 passed, 0 failed
  • All fixes verified: Yes

This PR should now be ready for merge.


This followup was generated by the gr-pr-followup skill.

## PR Review Followup Responding to review posted on 2026-03-17 ([review comment](https://git.bros.ninja/mike/open-project-sidecar-mcp/pulls/42#issuecomment-1043)) and [Mike's feedback](https://git.bros.ninja/mike/open-project-sidecar-mcp/pulls/42#issuecomment-1047). ### Triage Summary | Severity | Count | Action | | -------- | ----- | ------ | | Important | 1 | Fixed in this branch (prior commit) | | Minor | 2 | Informational — no action needed | | Nitpick | 1 | Fixed per Mike's direction | | Human Review | 1 | Resolved: custom field restore added | ### Fixes Applied 1. **[IMPORTANT]** Path traversal in `backup_file` parameter — fixed in 6d12cb3 - Sanitized with `pathMod.basename()` to strip directory traversal sequences 2. **[HUMAN REVIEW → FIXED]** Custom field restoration — fixed in 7bb2f0f - OP#2399: Restore custom fields in restore_deleted_work_packages - Restores scalar custom fields (string, int, float, date, bool) as top-level properties - Restores list-type custom fields via `custom_options` links ### Not Actioned - **[MINOR]** Timestamp regex drops milliseconds — cosmetic, display is still useful - **[MINOR]** appendFileSync in async context — consistent within setup wizard conventions - **[NITPICK]** list_delete_backups reads all files — negligible at default 20-file retention ### Test Results - **Tests:** 287 passed, 0 failed - **All fixes verified:** Yes This PR should now be ready for merge. --- _This followup was generated by the gr-pr-followup skill._
Mike Bros approved these changes 2026-03-18 00:21:58 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mike/open-project-sidecar-mcp!42
No description provided.