@wipcomputer/wip-ldm-os 0.4.86 → 0.4.87-alpha.2

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.
@@ -1,112 +1,129 @@
1
1
  # Backup: Technical Details
2
2
 
3
- ## Config Schema
4
-
5
- All backup settings are in `~/.ldm/config.json`. The backup script reads these at runtime.
6
-
7
- ```json
8
- {
9
- "org": "wipcomputerinc",
10
- "paths": {
11
- "workspace": "~/wipcomputerinc",
12
- "ldm": "~/.ldm",
13
- "claude": "~/.claude",
14
- "openclaw": "~/.openclaw",
15
- "icloudBackup": "~/Library/Mobile Documents/com~apple~CloudDocs/wipcomputerinc-icloud/backups"
16
- },
17
- "backup": {
18
- "keep": 7,
19
- "includeSecrets": false
20
- }
21
- }
22
- ```
3
+ ## Installed components
23
4
 
24
- | Key | Type | Default | Description |
25
- |-----|------|---------|-------------|
26
- | `paths.workspace` | string | required | Root workspace directory to back up |
27
- | `paths.icloudBackup` | string | optional | iCloud destination for offsite copies |
28
- | `backup.keep` | number | 7 | Days of backups to keep before rotation |
29
- | `backup.includeSecrets` | boolean | false | Whether to include `~/.ldm/secrets/` |
30
- | `org` | string | required | Used as prefix in iCloud tar filenames |
31
-
32
- ## Script Location
33
-
34
- - **Source:** `scripts/ldm-backup.sh` in the wip-ldm-os-private repo
35
- - **Deployed to:** `~/.ldm/bin/ldm-backup.sh`
36
- - **Deployed by:** `deployScripts()` in `bin/ldm.js`, called during both `ldm init` and `ldm install`
37
- - **Restore script:** `scripts/ldm-restore.sh` deployed to `~/.ldm/bin/ldm-restore.sh`
38
-
39
- All `.sh` files in the repo's `scripts/` directory are deployed to `~/.ldm/bin/` on every `ldm install`. This means script fixes land automatically on the next update without requiring a full `ldm init`.
40
-
41
- ## LaunchAgent
42
-
43
- **Label:** `ai.openclaw.ldm-backup`
44
- **Plist source:** `shared/launchagents/ai.openclaw.ldm-backup.plist`
45
- **Deployed to:** `~/Library/LaunchAgents/ai.openclaw.ldm-backup.plist`
46
-
47
- ```xml
48
- <key>StartCalendarInterval</key>
49
- <dict>
50
- <key>Hour</key>
51
- <integer>3</integer>
52
- <key>Minute</key>
53
- <integer>0</integer>
54
- </dict>
55
- ```
5
+ | Source | Installed path | Purpose |
6
+ |---|---|---|
7
+ | `scripts/ldm-backup.sh` | `~/.ldm/bin/ldm-backup.sh` | Stable shell entry point |
8
+ | `scripts/ldm-backup-engine.py` | `~/.ldm/bin/ldm-backup-engine.py` | Policy, planning, capture, validation, state, and retention engine |
9
+ | `scripts/ldm-backup-policy.json` | `~/.ldm/bin/ldm-backup-policy.json` | Canonical source and exclusion policy |
10
+
11
+ The package bin manifest owns all three files. `ldm install` deploys code only. It does not run a backup, create a destination, change the LaunchAgent, enable a schedule, or write a containment marker.
12
+
13
+ The engine runtime is Python 3 standard library only. The shell entry point checks for `python3` and emits the structured failure code `python3-missing` before execution when it is unavailable. No pip package or third-party Python dependency is introduced.
14
+
15
+ ## Canonical policy schema
16
+
17
+ Policy schema version 1 contains:
18
+
19
+ - `rule_version`
20
+ - Explicit `evidence_status_values`
21
+ - Disk floors and start reserve
22
+ - Source ID, root, relative path, safe display path, destination, type, classification, required flag, and action
23
+ - Block reason when encryption or reconstruction proof is missing
24
+ - Exclusion pattern, reason, reconstruction source, and rule version
25
+
26
+ Legal actions are `capture`, `blocked`, `manifest`, and `exclude`. Legal types are `file`, `tree`, and `sqlite`. The placeholder string `complete-or-deferred-after-write-failure` is rejected by policy and runtime state validation.
27
+
28
+ The production policy has an explicit ID for every controlling Slice 1 source row. A fixture asserts exact set equality, not subset membership, so either an omission or an unreviewed addition fails loudly.
29
+
30
+ `manifest` requires a named evidence generator. The implemented `path-inventory` generator records the source type, exact members, member count, logical bytes, and SHA-256 of the serialized inventory. A missing source, generator failure, malformed record, or missing required evidence prevents completion. Required evidence without a complete Slice 2 generator uses `blocked`, including repo-aware Git refs and worktree state, macOS system facts, package inventories, and out-of-band 1Password bootstrap proof.
31
+
32
+ ## Planning and dry run
33
+
34
+ Planning resolves the configured roots, expands each policy entry, selects files using its exclusion rules, and computes checksums and logical sizes. `estimated_new_bytes` is the exact logical payload plus the serialized manifest and completion marker for an unchanged source set. The disk reserve is evaluated against that plan-time value.
56
35
 
57
- The plist uses `{{HOME}}` placeholders that are replaced at deploy time by `ldm init`.
36
+ `--dry-run` stops after planning and preflight. It does not acquire the persistent lock or call any atomic writer. JSON output is available with `--dry-run --json` for reviewers and tests.
58
37
 
59
- **Logs:** stdout and stderr both go to `~/.ldm/logs/backup.log`.
38
+ Configuration loading is fail-closed. A missing config permits documented defaults. An unreadable file, malformed JSON, a non-object root, or non-object `paths` or `backup` field produces `config-invalid`.
60
39
 
61
- **No Full Disk Access (FDA):** The LaunchAgent runs at 3:00 AM without FDA. Some paths (like `~/Library/Messages/`) are inaccessible without FDA. The target is to move the trigger to midnight via LDMDevTools.app (which has FDA) once the PID error is resolved.
40
+ ## Mutation-during-run contract
62
41
 
63
- ### Dead Triggers (Cleaned Automatically)
42
+ Planning is not treated as a filesystem freeze. Immediately before each source capture, the engine rebuilds that source observation from the same canonical policy. Plan-to-capture changes are allowed and recorded through planned size, modification time, checksum, capture-time size, capture-time modification time, destination checksum, and `changed_since_plan`.
64
43
 
65
- The `cleanDeadBackupTriggers()` function in `ldm.js` removes old competing triggers on every `ldm init`:
66
- - Old cron entries referencing `LDMDevTools.app`
67
- - `com.wipcomputer.daily-backup` LaunchAgent
68
- - OpenClaw `backup-verify` cron entries
44
+ For regular files and trees, the capture-time observation is the source correspondence proof. Destination bytes are compared to that observation, and the manifest records per-source member counts, payload bytes, and inventory fingerprints for both planning and capture. After copying the full source, the engine observes it again. Any member addition, removal, type change, size change, modification-time change, or checksum change during capture fails with `source-changed-during-capture`. Same-size content changes are therefore detected. A plan-time change does not force failure merely because an active source changed before its capture began.
69
45
 
70
- Only `ai.openclaw.ldm-backup` should exist.
46
+ SQLite uses a separate contract. Python's SQLite backup API produces a transactionally consistent destination snapshot. The manifest labels these records `sqlite-backup-api-transaction-snapshot`; it does not claim that the destination checksum equals a raw live database file checksum.
71
47
 
72
- ## Rotation Logic
48
+ Before copying each source, the engine refreshes size and modification-time estimates for every capture source still pending without hashing each remaining tree. It then performs the full member and checksum observation for the source about to be copied. The engine recalculates the reserve from bytes already staged, refreshed remaining payload, projected manifest and completion metadata, current available space, and the required reserve. Metadata projection updates only changed source and evidence records instead of rebuilding the complete manifest for every source. A failed recheck raises `capture-reserve-gate` before the changed source is copied and records the updated estimate and reserve in `result.json`.
73
49
 
74
- The backup script handles rotation after a successful backup:
50
+ Manifest actions are gated before any source path is inspected or walked. An entry without an implemented generator is reported as blocked immediately. This prevents generator-less entries from hashing large trees or failing on special files that the unused source path happens to contain.
75
51
 
76
- 1. List all dated directories in `~/.ldm/backups/` (format: `YYYY-MM-DD--HH-MM-SS`)
77
- 2. Sort by name (which sorts chronologically)
78
- 3. Skip any directory containing a `.pinned` marker file
79
- 4. Delete directories beyond the `keep` count (oldest first)
80
- 5. Same rotation logic applies to iCloud tars at `paths.icloudBackup`
52
+ ## Run lifecycle
81
53
 
82
- **Pinning:** `ldm backup --pin "reason"` creates a `.pinned` file in the latest backup directory. Pinned backups are never rotated.
54
+ 1. Create the lock with `O_CREAT | O_EXCL`.
55
+ 2. Reject an active lock. Report a dead-PID lock as stale without replacing it.
56
+ 3. Build the plan from the canonical policy.
57
+ 4. Atomically write `schedule.json` and `attempt.json` before staging.
58
+ 5. Fail preflight before any staging directory when a required gate, destination, or reserve check fails.
59
+ 6. Create `.in-progress-<snapshot-id>`.
60
+ 7. Generate and validate required manifest evidence.
61
+ 8. Re-observe each capture source and all remaining capture sources.
62
+ 9. Recalculate the disk reserve before copying the current source.
63
+ 10. Capture SQLite databases with Python's SQLite backup API and run `PRAGMA quick_check` on each destination.
64
+ 11. Re-observe regular-file and tree sources after capture and fail if they changed during capture.
65
+ 12. Atomically write `manifest.json`.
66
+ 13. Validate every required capture record and manifest-evidence record.
67
+ 14. Atomically write `completion.json` last.
68
+ 15. Verify completion proof, then atomically promote staging.
69
+ 16. Write `result.json` and `success-local.json`.
70
+ 17. Rotate only eligible verified snapshots.
83
71
 
84
- ## iCloud Offsite Details
72
+ Failures do not promote staging and do not print `Backup complete`. A process crash can leave only an unmistakable in-progress directory and stale lock.
85
73
 
86
- After the local backup completes:
74
+ ## Manifest fields
87
75
 
88
- 1. Tar + gzip the entire dated backup directory
89
- 2. Filename format: `<org>-<machine>-<timestamp>.tar.gz`
90
- 3. Copy to `paths.icloudBackup` (from config.json)
91
- 4. Apply the same rotation (keep N, skip pinned)
92
- 5. iCloud syncs the file to all devices automatically
76
+ Each captured member records:
93
77
 
94
- The iCloud path must exist. The script does not create it. `ldm init` does not create it either. Create it manually if it does not exist.
78
+ - Source ID and resolved source path
79
+ - Relative destination
80
+ - Type
81
+ - Logical size
82
+ - Modification time in nanoseconds
83
+ - SHA-256 checksum
84
+ - Plan-time size, modification time, and checksum where meaningful
85
+ - Whether the source changed between planning and capture
86
+ - The source consistency contract
95
87
 
96
- ## SQLite Safety
88
+ The manifest also records schema version, UTC snapshot ID, tool version, policy rule version, timestamps, planned and captured payload bytes, required manifest evidence, exclusions, and terminal result. Each exclusion records source ID, path or pattern, reason, reconstruction source, rule version, and matched count.
97
89
 
98
- SQLite files are backed up using `sqlite3 .backup`, not `cp`. This ensures a consistent snapshot even if the database is being written to. The script checks for the `sqlite3` binary and skips database backup with a warning if it is not found.
90
+ `completion.json` records snapshot ID, `status: complete`, completion time, and manifest checksum. Its presence alone is insufficient unless the manifest checksum and terminal result validate.
99
91
 
100
- Files backed up this way:
101
- - `~/.ldm/memory/crystal.db`
102
- - `~/.openclaw/memory/main.sqlite`
103
- - `~/.openclaw/memory/context-embeddings.sqlite`
92
+ ## Structured state
104
93
 
105
- ## Excludes
94
+ All state files use same-directory temporary files, `fsync`, and atomic rename.
95
+
96
+ | File | Meaning |
97
+ |---|---|
98
+ | `schedule.json` | Schedule ID, timezone, expected slot, containment status, and marker-derived evidence status or `unknown` |
99
+ | `attempt.json` | Run ID, PID, tool version, start time, and preflight values |
100
+ | `result.json` | Terminal result, failure code, estimates, and per-tier status |
101
+ | `success-local.json` | Last verified internal success |
102
+ | `success-vault.json` | Reserved for Slice 3 |
103
+ | `success-icloud.json` | Reserved for Slice 4 |
104
+
105
+ These records are diagnostic. They do not replace snapshot completion proof.
106
+
107
+ ## Retention
108
+
109
+ Retention considers a directory eligible only when:
110
+
111
+ - It is not named `.in-progress-*`.
112
+ - `manifest.json` has `terminal_result: success`.
113
+ - `completion.json` has `status: complete`.
114
+ - The completion marker matches the manifest SHA-256.
115
+ - `.pinned` is absent.
116
+
117
+ The engine keeps the configured number of unpinned eligible snapshots plus every pinned snapshot. Unverified directories are preserved for later disposition.
118
+
119
+ ## Fixture isolation
120
+
121
+ Tests set `LDM_BACKUP_REQUIRE_FIXTURE=1` and provide a unique `LDM_BACKUP_FIXTURE_ROOT` whose basename starts with `ldm-backup-fixture-`. Before planning or writing, the engine proves that all injected home, LDM, OpenClaw, Claude, Codex, workspace, destination, state, iCloud substitute, and lock paths resolve beneath that root.
122
+
123
+ The entire fixture suite runs with:
124
+
125
+ ```bash
126
+ npm run test:backup-engine
127
+ ```
106
128
 
107
- The workspace tar excludes:
108
- - `node_modules/` ... reconstructable via npm install
109
- - `.git/objects/` ... reconstructable via git fetch
110
- - `backups/` ... avoids recursive backup
111
- - `_trash/` ... already deleted content
112
- - `*.tar.gz` ... avoids backing up old backup archives
129
+ It covers success, dry-run parity, exact policy IDs, required manifest evidence and generator failures, generator-less manifest short-circuiting over a tree containing a Unix socket, plan-to-capture size and same-size changes, capture-time reserve success and refusal, fatal during-capture mutation, concurrent and stale locks, missing and blocked required sources, SQLite failure, simulated ENOSPC, preflight failure, crash staging, atomic state, completion proof, configuration failures, UTC IDs, evidence status, retention, and path escape refusal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.86",
3
+ "version": "0.4.87-alpha.2",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "build:bridge": "cd src/bridge && npm install && npx tsup core.ts mcp-server.ts cli.ts openclaw.ts --format esm --dts --clean --outDir ../../dist/bridge",
20
20
  "build": "npm run build:bridge",
21
- "prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:readme-install-prompt && npm run test:ldm-status-timeout && npm run test:ldm-status-concurrency && npm run test:legacy-npm-sources-migration",
21
+ "prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:readme-install-prompt && npm run test:ldm-status-timeout && npm run test:ldm-status-concurrency && npm run test:legacy-npm-sources-migration && npm run test:ldm-install-bin-shim && npm run test:backup-engine",
22
22
  "validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
23
23
  "test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
24
24
  "test:install-prompt-policy": "node scripts/test-install-prompt-policy.mjs",
@@ -50,7 +50,9 @@
50
50
  "test:boot-hook-registration": "node scripts/test-boot-hook-registration.mjs",
51
51
  "test:doctor-hook-dedupe": "node scripts/test-doctor-hook-dedupe.mjs",
52
52
  "test:boot-dir-truth": "node scripts/test-boot-dir-truth.mjs",
53
- "test:deploy-hook-ownership": "node scripts/test-deploy-hook-ownership.mjs"
53
+ "test:deploy-hook-ownership": "node scripts/test-deploy-hook-ownership.mjs",
54
+ "test:doctor-commit-deployed": "node scripts/test-doctor-commit-deployed.mjs",
55
+ "test:backup-engine": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-backup-engine.py"
54
56
  },
55
57
  "claudeCode": {
56
58
  "hook": {
@@ -72,6 +74,17 @@
72
74
  "source": "scripts/ldm-backup.sh",
73
75
  "purpose": "daily backup; LaunchAgent at 03:00"
74
76
  },
77
+ {
78
+ "name": "ldm-backup-engine.py",
79
+ "source": "scripts/ldm-backup-engine.py",
80
+ "purpose": "policy-driven backup engine"
81
+ },
82
+ {
83
+ "name": "ldm-backup-policy.json",
84
+ "source": "scripts/ldm-backup-policy.json",
85
+ "executable": false,
86
+ "purpose": "canonical backup source policy"
87
+ },
75
88
  {
76
89
  "name": "ldm-restore.sh",
77
90
  "source": "scripts/ldm-restore.sh",