openclaw-workspace-sync 2.1.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,13 +56,26 @@ On startup, the plugin bootstraps both directories:
56
56
 
57
57
  Because the push explicitly excludes `_inbox/**` and `_outbox/**`, there is no risk of sync loops or accidental overwrites. Files only flow in one direction through each channel.
58
58
 
59
+ #### Inbox notifications (optional)
60
+
61
+ By default, mailbox mode is silent — files land in `_inbox` without waking the agent. This keeps costs at zero.
62
+
63
+ If you want the agent to react when files arrive, set `"notifyOnInbox": true`. After each drain that moves files, the plugin injects a system event like:
64
+
65
+ > `[workspace-sync] New files in _inbox: report.pdf, data.csv`
66
+
67
+ This wakes the agent on its next heartbeat. The agent sees the message and can process the files — for example, reading, summarizing, or filing them.
68
+
69
+ **This costs credits.** Each notification triggers an agent turn. Only enable it if you want the agent to actively respond to incoming files.
70
+
59
71
  ```json
60
72
  {
61
73
  "mode": "mailbox",
62
74
  "provider": "dropbox",
63
75
  "remotePath": "",
64
76
  "localPath": "/",
65
- "interval": 60
77
+ "interval": 180,
78
+ "notifyOnInbox": true
66
79
  }
67
80
  ```
68
81
 
@@ -208,15 +221,28 @@ Add to your `openclaw.json`:
208
221
  "openclaw-workspace-sync": {
209
222
  "enabled": true,
210
223
  "config": {
211
- "provider": "dropbox",
212
- "mode": "mailbox",
213
- "remotePath": "",
214
- "localPath": "/",
215
- "interval": 60,
216
- "timeout": 1800,
217
- "onSessionStart": true,
218
- "onSessionEnd": false,
219
- "exclude": [".git/**", "node_modules/**", "*.log"]
224
+ "sync": {
225
+ "provider": "dropbox",
226
+ "mode": "mailbox",
227
+ "remotePath": "",
228
+ "localPath": "/",
229
+ "interval": 60,
230
+ "timeout": 1800,
231
+ "onSessionStart": true,
232
+ "onSessionEnd": false,
233
+ "exclude": [".git/**", "node_modules/**", "*.log"]
234
+ },
235
+ "backup": {
236
+ "enabled": true,
237
+ "provider": "s3",
238
+ "bucket": "my-backups",
239
+ "prefix": "habibi/",
240
+ "interval": 86400,
241
+ "encrypt": true,
242
+ "passphrase": "${BACKUP_PASSPHRASE}",
243
+ "include": ["workspace", "config", "cron", "memory"],
244
+ "retain": 7
245
+ }
220
246
  }
221
247
  }
222
248
  }
@@ -224,6 +250,8 @@ Add to your `openclaw.json`:
224
250
  }
225
251
  ```
226
252
 
253
+ > **Flat format still works.** Putting `provider`, `mode`, etc. at the config root (without `sync`) is supported for backwards compatibility. The nested `{ sync, backup }` format is recommended for clarity.
254
+
227
255
  ### Config reference
228
256
 
229
257
  | Key | Type | Default | Description |
@@ -232,6 +260,7 @@ Add to your `openclaw.json`:
232
260
  | `mode` | string | **required** | `mailbox` \| `mirror` \| `bisync` — see [Sync modes](#sync-modes-breaking-change-in-v20) |
233
261
  | `ingest` | boolean | `false` | Enable local inbox for sending files to the agent (mirror mode only) |
234
262
  | `ingestPath` | string | `"inbox"` | Local subfolder name for ingestion (relative to `localPath`) |
263
+ | `notifyOnInbox` | boolean | `false` | Wake the agent when files arrive in `_inbox` (mailbox mode). Off by default — enabling this costs LLM credits per notification. |
235
264
  | `remotePath` | string | `"openclaw-share"` | Folder name in cloud storage |
236
265
  | `localPath` | string | `"shared"` | Subfolder within workspace to sync |
237
266
  | `interval` | number | `0` | Background sync interval in seconds (0 = manual only, min 60) |
@@ -447,6 +476,14 @@ Benefits:
447
476
  - If token is compromised, blast radius is limited
448
477
  - Clean separation — sync folder lives under `Apps/<your-app-name>/`
449
478
 
479
+ ### Dropbox rate limiting
480
+
481
+ Dropbox enforces API rate limits (`too_many_requests`). If your workspace has many files (10k+), each sync cycle can consume a large number of API calls just for checking. To avoid hitting limits:
482
+
483
+ - **Set `interval` high enough** for the sync to complete between cycles. A workspace with ~40k files takes ~2 minutes to scan. An `interval` of 180 (3 min) is the minimum; 300 (5 min) is safer.
484
+ - **Use `exclude` patterns liberally** — skip `node_modules`, `.git`, `__pycache__`, build output, and anything you don't need synced. Fewer files = fewer API calls.
485
+ - **If you see `too_many_requests` errors** in the logs, increase the interval and add more excludes.
486
+
450
487
  ## Understanding sync safety
451
488
 
452
489
  Cloud sync involves two copies of your data. When things go wrong, one side can overwrite the other. Here is what to keep in mind:
@@ -541,6 +578,171 @@ These recommendations apply regardless of whether you use this plugin. Cloud syn
541
578
  - **Encryption**: Consider [rclone crypt](https://rclone.org/crypt/) for sensitive data
542
579
  - **App folder**: Use Dropbox app folder access for minimal permissions
543
580
 
581
+ ## Encrypted backups
582
+
583
+ Back up your entire agent system — workspace, config, cron jobs, memory, sessions — as encrypted snapshots to your own cloud storage. Your bucket, your encryption key, zero monthly fees.
584
+
585
+ ### How it works
586
+
587
+ <p align="center">
588
+ <img src="https://raw.githubusercontent.com/ashbrener/openclaw-workspace-sync/main/docs/diagrams/mode-3.svg" alt="backup pipeline diagram" width="700" />
589
+ </p>
590
+
591
+ 1. **Streams directly** — `tar | [openssl enc] | rclone rcat` piped straight to the remote. Zero local temp files, zero extra disk needed. A 10 GB workspace on a 1 GB free volume? No problem.
592
+ 2. Optionally encrypts with AES-256 (client-side, before upload) via `openssl`
593
+ 3. Uploads via rclone to any supported provider (S3, R2, Backblaze B2, Dropbox, etc.)
594
+ 4. Prunes old snapshots based on your retention policy
595
+
596
+ > **Disk-constrained?** Because backups stream directly, you don't need any free disk space for the backup itself. Only the restore downloads to a staging directory.
597
+
598
+ ### Configuration
599
+
600
+ Add `sync` and `backup` blocks to your plugin config. The backup provider can differ from your sync provider — sync to Dropbox for live mirror, backup to S3 for disaster recovery:
601
+
602
+ ```json
603
+ {
604
+ "sync": {
605
+ "provider": "dropbox",
606
+ "mode": "mailbox",
607
+ "remotePath": "",
608
+ "interval": 180
609
+ },
610
+ "backup": {
611
+ "enabled": true,
612
+ "provider": "s3",
613
+ "bucket": "my-backups",
614
+ "prefix": "habibi/",
615
+ "interval": 86400,
616
+ "encrypt": true,
617
+ "passphrase": "${BACKUP_PASSPHRASE}",
618
+ "include": ["workspace", "config", "cron", "memory"],
619
+ "retain": { "daily": 7, "weekly": 4 }
620
+ }
621
+ }
622
+ ```
623
+
624
+ ### Backup config reference
625
+
626
+ | Key | Type | Default | Description |
627
+ |-----|------|---------|-------------|
628
+ | `enabled` | boolean | `false` | Enable scheduled backups |
629
+ | `provider` | string | parent provider | Cloud provider for backup storage |
630
+ | `bucket` | string | — | S3/R2 bucket name |
631
+ | `prefix` | string | `""` | Path prefix within the bucket (e.g., `habibi/`) |
632
+ | `interval` | number | `86400` | Backup interval in seconds (86400 = daily; clamped to min 300) |
633
+ | `encrypt` | boolean | `false` | Encrypt snapshots with AES-256 before upload |
634
+ | `passphrase` | string | — | Encryption passphrase (use `${BACKUP_PASSPHRASE}` env var) |
635
+ | `include` | string[] | see below | What to back up |
636
+ | `retain` | number or object | `7` | Retention: `7` = keep 7 latest, or `{ daily: 7, weekly: 4 }` |
637
+ | `exclude` | string[] | parent excludes | Glob patterns to exclude from workspace backup |
638
+
639
+ ### Include options
640
+
641
+ | Item | What gets backed up |
642
+ |------|---------------------|
643
+ | `workspace` | The workspace directory (agent files, projects, etc.) |
644
+ | `config` | `openclaw.json` |
645
+ | `cron` | Cron job schedules and state |
646
+ | `memory` | Memory files (MEMORY.md, etc.) |
647
+ | `sessions` | Session metadata and store |
648
+ | `credentials` | Auth profile credentials |
649
+ | `skills` | Skill files |
650
+ | `hooks` | Webhook configurations and state (Gmail watch, custom hooks) |
651
+ | `extensions` | Installed plugins/extensions (for reproducible restores) |
652
+ | `env` | Environment variables file (`.env`) |
653
+ | `agents` | Multi-agent state (per-agent sessions, subagent registry) |
654
+ | `pages` | Custom pages served by the gateway |
655
+ | `transcripts` | Full conversation logs (JSONL session transcripts) |
656
+
657
+ Default: `["workspace", "config", "cron", "memory"]`
658
+
659
+ ### CLI commands
660
+
661
+ ```bash
662
+ # Create a backup now
663
+ openclaw workspace-sync backup now
664
+
665
+ # List available snapshots
666
+ openclaw workspace-sync backup list
667
+
668
+ # Restore the latest snapshot
669
+ openclaw workspace-sync backup restore
670
+
671
+ # Restore a specific snapshot
672
+ openclaw workspace-sync backup restore --snapshot backup-20260310T020000Z.tar.gz.enc
673
+
674
+ # Restore to a specific directory (safe — doesn't overwrite live data)
675
+ openclaw workspace-sync backup restore --to /tmp/restore-test
676
+
677
+ # Check backup service status
678
+ openclaw workspace-sync backup status
679
+ ```
680
+
681
+ ### Restore safety
682
+
683
+ By default, `restore` extracts to a staging directory (`~/.openclaw/.backup-restore/`), not directly over your live workspace. This lets you inspect the contents before copying them into place. Use `--to` to control where files land.
684
+
685
+ ### Provider examples
686
+
687
+ **Cloudflare R2 (free tier: 10GB):**
688
+
689
+ ```json
690
+ {
691
+ "backup": {
692
+ "enabled": true,
693
+ "provider": "s3",
694
+ "encrypt": true,
695
+ "passphrase": "${BACKUP_PASSPHRASE}",
696
+ "s3": {
697
+ "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
698
+ "bucket": "openclaw-backups",
699
+ "region": "auto",
700
+ "accessKeyId": "${R2_ACCESS_KEY}",
701
+ "secretAccessKey": "${R2_SECRET_KEY}"
702
+ }
703
+ }
704
+ }
705
+ ```
706
+
707
+ **AWS S3:**
708
+
709
+ ```json
710
+ {
711
+ "backup": {
712
+ "enabled": true,
713
+ "provider": "s3",
714
+ "encrypt": true,
715
+ "passphrase": "${BACKUP_PASSPHRASE}",
716
+ "s3": {
717
+ "bucket": "my-openclaw-backups",
718
+ "region": "us-east-1",
719
+ "accessKeyId": "${AWS_ACCESS_KEY_ID}",
720
+ "secretAccessKey": "${AWS_SECRET_ACCESS_KEY}"
721
+ }
722
+ }
723
+ }
724
+ ```
725
+
726
+ **Backblaze B2:**
727
+
728
+ ```json
729
+ {
730
+ "backup": {
731
+ "enabled": true,
732
+ "provider": "s3",
733
+ "encrypt": true,
734
+ "passphrase": "${BACKUP_PASSPHRASE}",
735
+ "s3": {
736
+ "endpoint": "https://s3.us-west-002.backblazeb2.com",
737
+ "bucket": "openclaw-backups",
738
+ "region": "us-west-002",
739
+ "accessKeyId": "${B2_KEY_ID}",
740
+ "secretAccessKey": "${B2_APP_KEY}"
741
+ }
742
+ }
743
+ }
744
+ ```
745
+
544
746
  ## Development
545
747
 
546
748
  ```bash
package/README.src.md CHANGED
@@ -72,13 +72,26 @@ On startup, the plugin bootstraps both directories:
72
72
 
73
73
  Because the push explicitly excludes `_inbox/**` and `_outbox/**`, there is no risk of sync loops or accidental overwrites. Files only flow in one direction through each channel.
74
74
 
75
+ #### Inbox notifications (optional)
76
+
77
+ By default, mailbox mode is silent — files land in `_inbox` without waking the agent. This keeps costs at zero.
78
+
79
+ If you want the agent to react when files arrive, set `"notifyOnInbox": true`. After each drain that moves files, the plugin injects a system event like:
80
+
81
+ > `[workspace-sync] New files in _inbox: report.pdf, data.csv`
82
+
83
+ This wakes the agent on its next heartbeat. The agent sees the message and can process the files — for example, reading, summarizing, or filing them.
84
+
85
+ **This costs credits.** Each notification triggers an agent turn. Only enable it if you want the agent to actively respond to incoming files.
86
+
75
87
  ```json
76
88
  {
77
89
  "mode": "mailbox",
78
90
  "provider": "dropbox",
79
91
  "remotePath": "",
80
92
  "localPath": "/",
81
- "interval": 60
93
+ "interval": 180,
94
+ "notifyOnInbox": true
82
95
  }
83
96
  ```
84
97
 
@@ -249,15 +262,28 @@ Add to your `openclaw.json`:
249
262
  "openclaw-workspace-sync": {
250
263
  "enabled": true,
251
264
  "config": {
252
- "provider": "dropbox",
253
- "mode": "mailbox",
254
- "remotePath": "",
255
- "localPath": "/",
256
- "interval": 60,
257
- "timeout": 1800,
258
- "onSessionStart": true,
259
- "onSessionEnd": false,
260
- "exclude": [".git/**", "node_modules/**", "*.log"]
265
+ "sync": {
266
+ "provider": "dropbox",
267
+ "mode": "mailbox",
268
+ "remotePath": "",
269
+ "localPath": "/",
270
+ "interval": 60,
271
+ "timeout": 1800,
272
+ "onSessionStart": true,
273
+ "onSessionEnd": false,
274
+ "exclude": [".git/**", "node_modules/**", "*.log"]
275
+ },
276
+ "backup": {
277
+ "enabled": true,
278
+ "provider": "s3",
279
+ "bucket": "my-backups",
280
+ "prefix": "habibi/",
281
+ "interval": 86400,
282
+ "encrypt": true,
283
+ "passphrase": "${BACKUP_PASSPHRASE}",
284
+ "include": ["workspace", "config", "cron", "memory"],
285
+ "retain": 7
286
+ }
261
287
  }
262
288
  }
263
289
  }
@@ -265,6 +291,8 @@ Add to your `openclaw.json`:
265
291
  }
266
292
  ```
267
293
 
294
+ > **Flat format still works.** Putting `provider`, `mode`, etc. at the config root (without `sync`) is supported for backwards compatibility. The nested `{ sync, backup }` format is recommended for clarity.
295
+
268
296
  ### Config reference
269
297
 
270
298
  | Key | Type | Default | Description |
@@ -273,6 +301,7 @@ Add to your `openclaw.json`:
273
301
  | `mode` | string | **required** | `mailbox` \| `mirror` \| `bisync` — see [Sync modes](#sync-modes-breaking-change-in-v20) |
274
302
  | `ingest` | boolean | `false` | Enable local inbox for sending files to the agent (mirror mode only) |
275
303
  | `ingestPath` | string | `"inbox"` | Local subfolder name for ingestion (relative to `localPath`) |
304
+ | `notifyOnInbox` | boolean | `false` | Wake the agent when files arrive in `_inbox` (mailbox mode). Off by default — enabling this costs LLM credits per notification. |
276
305
  | `remotePath` | string | `"openclaw-share"` | Folder name in cloud storage |
277
306
  | `localPath` | string | `"shared"` | Subfolder within workspace to sync |
278
307
  | `interval` | number | `0` | Background sync interval in seconds (0 = manual only, min 60) |
@@ -488,6 +517,14 @@ Benefits:
488
517
  - If token is compromised, blast radius is limited
489
518
  - Clean separation — sync folder lives under `Apps/<your-app-name>/`
490
519
 
520
+ ### Dropbox rate limiting
521
+
522
+ Dropbox enforces API rate limits (`too_many_requests`). If your workspace has many files (10k+), each sync cycle can consume a large number of API calls just for checking. To avoid hitting limits:
523
+
524
+ - **Set `interval` high enough** for the sync to complete between cycles. A workspace with ~40k files takes ~2 minutes to scan. An `interval` of 180 (3 min) is the minimum; 300 (5 min) is safer.
525
+ - **Use `exclude` patterns liberally** — skip `node_modules`, `.git`, `__pycache__`, build output, and anything you don't need synced. Fewer files = fewer API calls.
526
+ - **If you see `too_many_requests` errors** in the logs, increase the interval and add more excludes.
527
+
491
528
  ## Understanding sync safety
492
529
 
493
530
  Cloud sync involves two copies of your data. When things go wrong, one side can overwrite the other. Here is what to keep in mind:
@@ -582,6 +619,188 @@ These recommendations apply regardless of whether you use this plugin. Cloud syn
582
619
  - **Encryption**: Consider [rclone crypt](https://rclone.org/crypt/) for sensitive data
583
620
  - **App folder**: Use Dropbox app folder access for minimal permissions
584
621
 
622
+ ## Encrypted backups
623
+
624
+ Back up your entire agent system — workspace, config, cron jobs, memory, sessions — as encrypted snapshots to your own cloud storage. Your bucket, your encryption key, zero monthly fees.
625
+
626
+ ### How it works
627
+
628
+ ```mermaid
629
+ flowchart TD
630
+ subgraph GW["Gateway"]
631
+ WS["/workspace"]
632
+ CFG["config / cron / memory"]
633
+ end
634
+ TAR["tar cz"]
635
+ ENC["openssl enc\n(AES-256)"]
636
+ RCAT["rclone rcat"]
637
+ subgraph REMOTE["Your Bucket (S3 / R2 / B2)"]
638
+ SNAP["backup-2026…Z.tar.gz.enc"]
639
+ OLD["older snapshots"]
640
+ end
641
+ WS --> TAR
642
+ CFG --> TAR
643
+ TAR -- "pipe" --> ENC
644
+ ENC -- "pipe" --> RCAT
645
+ RCAT --> SNAP
646
+ SNAP -. "retention prune" .-> OLD
647
+ ```
648
+
649
+ 1. **Streams directly** — `tar | [openssl enc] | rclone rcat` piped straight to the remote. Zero local temp files, zero extra disk needed. A 10 GB workspace on a 1 GB free volume? No problem.
650
+ 2. Optionally encrypts with AES-256 (client-side, before upload) via `openssl`
651
+ 3. Uploads via rclone to any supported provider (S3, R2, Backblaze B2, Dropbox, etc.)
652
+ 4. Prunes old snapshots based on your retention policy
653
+
654
+ > **Disk-constrained?** Because backups stream directly, you don't need any free disk space for the backup itself. Only the restore downloads to a staging directory.
655
+
656
+ ### Configuration
657
+
658
+ Add `sync` and `backup` blocks to your plugin config. The backup provider can differ from your sync provider — sync to Dropbox for live mirror, backup to S3 for disaster recovery:
659
+
660
+ ```json
661
+ {
662
+ "sync": {
663
+ "provider": "dropbox",
664
+ "mode": "mailbox",
665
+ "remotePath": "",
666
+ "interval": 180
667
+ },
668
+ "backup": {
669
+ "enabled": true,
670
+ "provider": "s3",
671
+ "bucket": "my-backups",
672
+ "prefix": "habibi/",
673
+ "interval": 86400,
674
+ "encrypt": true,
675
+ "passphrase": "${BACKUP_PASSPHRASE}",
676
+ "include": ["workspace", "config", "cron", "memory"],
677
+ "retain": { "daily": 7, "weekly": 4 }
678
+ }
679
+ }
680
+ ```
681
+
682
+ ### Backup config reference
683
+
684
+ | Key | Type | Default | Description |
685
+ |-----|------|---------|-------------|
686
+ | `enabled` | boolean | `false` | Enable scheduled backups |
687
+ | `provider` | string | parent provider | Cloud provider for backup storage |
688
+ | `bucket` | string | — | S3/R2 bucket name |
689
+ | `prefix` | string | `""` | Path prefix within the bucket (e.g., `habibi/`) |
690
+ | `interval` | number | `86400` | Backup interval in seconds (86400 = daily; clamped to min 300) |
691
+ | `encrypt` | boolean | `false` | Encrypt snapshots with AES-256 before upload |
692
+ | `passphrase` | string | — | Encryption passphrase (use `${BACKUP_PASSPHRASE}` env var) |
693
+ | `include` | string[] | see below | What to back up |
694
+ | `retain` | number or object | `7` | Retention: `7` = keep 7 latest, or `{ daily: 7, weekly: 4 }` |
695
+ | `exclude` | string[] | parent excludes | Glob patterns to exclude from workspace backup |
696
+
697
+ ### Include options
698
+
699
+ | Item | What gets backed up |
700
+ |------|---------------------|
701
+ | `workspace` | The workspace directory (agent files, projects, etc.) |
702
+ | `config` | `openclaw.json` |
703
+ | `cron` | Cron job schedules and state |
704
+ | `memory` | Memory files (MEMORY.md, etc.) |
705
+ | `sessions` | Session metadata and store |
706
+ | `credentials` | Auth profile credentials |
707
+ | `skills` | Skill files |
708
+ | `hooks` | Webhook configurations and state (Gmail watch, custom hooks) |
709
+ | `extensions` | Installed plugins/extensions (for reproducible restores) |
710
+ | `env` | Environment variables file (`.env`) |
711
+ | `agents` | Multi-agent state (per-agent sessions, subagent registry) |
712
+ | `pages` | Custom pages served by the gateway |
713
+ | `transcripts` | Full conversation logs (JSONL session transcripts) |
714
+
715
+ Default: `["workspace", "config", "cron", "memory"]`
716
+
717
+ ### CLI commands
718
+
719
+ ```bash
720
+ # Create a backup now
721
+ openclaw workspace-sync backup now
722
+
723
+ # List available snapshots
724
+ openclaw workspace-sync backup list
725
+
726
+ # Restore the latest snapshot
727
+ openclaw workspace-sync backup restore
728
+
729
+ # Restore a specific snapshot
730
+ openclaw workspace-sync backup restore --snapshot backup-20260310T020000Z.tar.gz.enc
731
+
732
+ # Restore to a specific directory (safe — doesn't overwrite live data)
733
+ openclaw workspace-sync backup restore --to /tmp/restore-test
734
+
735
+ # Check backup service status
736
+ openclaw workspace-sync backup status
737
+ ```
738
+
739
+ ### Restore safety
740
+
741
+ By default, `restore` extracts to a staging directory (`~/.openclaw/.backup-restore/`), not directly over your live workspace. This lets you inspect the contents before copying them into place. Use `--to` to control where files land.
742
+
743
+ ### Provider examples
744
+
745
+ **Cloudflare R2 (free tier: 10GB):**
746
+
747
+ ```json
748
+ {
749
+ "backup": {
750
+ "enabled": true,
751
+ "provider": "s3",
752
+ "encrypt": true,
753
+ "passphrase": "${BACKUP_PASSPHRASE}",
754
+ "s3": {
755
+ "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
756
+ "bucket": "openclaw-backups",
757
+ "region": "auto",
758
+ "accessKeyId": "${R2_ACCESS_KEY}",
759
+ "secretAccessKey": "${R2_SECRET_KEY}"
760
+ }
761
+ }
762
+ }
763
+ ```
764
+
765
+ **AWS S3:**
766
+
767
+ ```json
768
+ {
769
+ "backup": {
770
+ "enabled": true,
771
+ "provider": "s3",
772
+ "encrypt": true,
773
+ "passphrase": "${BACKUP_PASSPHRASE}",
774
+ "s3": {
775
+ "bucket": "my-openclaw-backups",
776
+ "region": "us-east-1",
777
+ "accessKeyId": "${AWS_ACCESS_KEY_ID}",
778
+ "secretAccessKey": "${AWS_SECRET_ACCESS_KEY}"
779
+ }
780
+ }
781
+ }
782
+ ```
783
+
784
+ **Backblaze B2:**
785
+
786
+ ```json
787
+ {
788
+ "backup": {
789
+ "enabled": true,
790
+ "provider": "s3",
791
+ "encrypt": true,
792
+ "passphrase": "${BACKUP_PASSPHRASE}",
793
+ "s3": {
794
+ "endpoint": "https://s3.us-west-002.backblazeb2.com",
795
+ "bucket": "openclaw-backups",
796
+ "region": "us-west-002",
797
+ "accessKeyId": "${B2_KEY_ID}",
798
+ "secretAccessKey": "${B2_APP_KEY}"
799
+ }
800
+ }
801
+ }
802
+ ```
803
+
585
804
  ## Development
586
805
 
587
806
  ```bash
@@ -85,6 +85,26 @@ rclone sync <remote>:<path> /data/workspace/ --config <config-path> --verbose
85
85
  # Ctrl+B, D to detach; tmux attach -t sync to reconnect
86
86
  ```
87
87
 
88
+ ### Dropbox rate limiting (`too_many_requests`)
89
+
90
+ **Cause:** Dropbox enforces API rate limits. Each sync cycle checks every file in the workspace, so large workspaces (10k+ files) use many API calls. If the sync interval is shorter than the scan time, cycles overlap and requests pile up.
91
+
92
+ **Symptoms:** Logs show `NOTICE: Error too_many_requests/... Trying again in 300 seconds`. Sync stalls for 5 minutes waiting for the retry.
93
+
94
+ **Fix:**
95
+ 1. Increase `interval` — if a scan takes ~2 minutes, set interval to at least 180 (3 min), ideally 300 (5 min)
96
+ 2. Add more `exclude` patterns to reduce the number of files scanned:
97
+ ```json
98
+ "exclude": [
99
+ "**/node_modules/**",
100
+ "**/.git/**",
101
+ "**/__pycache__/**",
102
+ "**/dist/**",
103
+ "**/.cache/**"
104
+ ]
105
+ ```
106
+ 3. If already rate-limited, wait 5–10 minutes before the next cycle — rclone retries automatically
107
+
88
108
  ### "directory not found" with Dropbox app folder
89
109
 
90
110
  **Cause:** Your `remotePath` is set to the app folder name (e.g. `"openclaw-sync"`) instead of `""`. With Dropbox app folders, the app folder IS the root — rclone sees it as `/`.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Backup manager — encrypted snapshot backups to cloud storage.
3
+ *
4
+ * Streams tar | [openssl enc] | rclone rcat directly to the remote —
5
+ * zero local disk usage. Optionally encrypts with AES-256 via openssl.
6
+ * Prunes old snapshots based on retention policy.
7
+ */
8
+ import type { WorkspaceSyncConfig } from "./types.js";
9
+ type Logger = {
10
+ debug?: (msg: string) => void;
11
+ info: (msg: string) => void;
12
+ warn: (msg: string) => void;
13
+ error: (msg: string) => void;
14
+ };
15
+ export type BackupResult = {
16
+ ok: true;
17
+ snapshotName: string;
18
+ encrypted: boolean;
19
+ sizeBytes: number;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ export type RestoreResult = {
25
+ ok: true;
26
+ snapshotName: string;
27
+ restoredTo: string;
28
+ } | {
29
+ ok: false;
30
+ error: string;
31
+ };
32
+ export declare function runBackup(params: {
33
+ syncConfig: WorkspaceSyncConfig;
34
+ workspaceDir: string;
35
+ stateDir: string;
36
+ logger: Logger;
37
+ }): Promise<BackupResult>;
38
+ export declare function listBackupSnapshots(params: {
39
+ syncConfig: WorkspaceSyncConfig;
40
+ stateDir: string;
41
+ logger: Logger;
42
+ }): Promise<{
43
+ ok: true;
44
+ snapshots: string[];
45
+ } | {
46
+ ok: false;
47
+ error: string;
48
+ }>;
49
+ export declare function runRestore(params: {
50
+ syncConfig: WorkspaceSyncConfig;
51
+ workspaceDir: string;
52
+ stateDir: string;
53
+ snapshotName: string | "latest";
54
+ restoreTo: string;
55
+ logger: Logger;
56
+ }): Promise<RestoreResult>;
57
+ export declare function startBackupManager(syncConfig: WorkspaceSyncConfig, workspaceDir: string, stateDir: string, logger: Logger): void;
58
+ export declare function stopBackupManager(): void;
59
+ export declare function getBackupManagerStatus(): {
60
+ running: boolean;
61
+ lastBackupAt: Date | null;
62
+ lastBackupOk: boolean | null;
63
+ backupCount: number;
64
+ errorCount: number;
65
+ };
66
+ export {};
67
+ //# sourceMappingURL=backup-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-manager.d.ts","sourceRoot":"","sources":["../src/backup-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAiD,mBAAmB,EAAyB,MAAM,YAAY,CAAC;AAa5H,KAAK,MAAM,GAAG;IACZ,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAAC;AAiTF,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,UAAU,EAAE,mBAAmB,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,YAAY,CAAC,CAiFxB;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,mBAAmB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAe5E;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE;IACvC,UAAU,EAAE,mBAAmB,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAoGzB;AAoED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,mBAAmB,EAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,IAAI,CAyBN;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAUxC;AAED,wBAAgB,sBAAsB,IAAI;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB,CAQA"}