Implement versiond M1: inotify monitor, versioning, diff, restore, systemd install

FastAPI service on 127.0.0.1:9922 that monitors user-chosen directories
with pruned inotify watches, stores versions in a local content-addressed
spool indexed in SQLite, coalesces bursts (first + last), and offers
history, diff and dry-run-first bulk restore. Includes a CLI with a
systemd user unit installer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
retoor
2026-10-08 18:46:12 +02:00
co-authored by Claude Opus 5.5
parent 3455499cea
commit ddf09bea88
17 changed files with 2932 additions and 5 deletions
+28 -5
View File
@@ -6,6 +6,32 @@
---
## 0. Implementation Status & Quick Start
| Milestone | State |
|---|---|
| M1 – Core (monitor, filters, index, spool, history, diff, restore, systemd install) | **Implemented** (`src/versiond/`) |
| M2 – WebDAV remote, unique remote directory, encryption | Not started. Versions are stored locally (`durability = local`) |
| M3 – Retention, purge, GC | Not started (pinning exists) |
| M4 – Agent skill, metrics, reindex/adopt | Not started |
```bash
pipx install -e . # installs the `versiond` command
versiond install # writes the user unit, enables lingering, starts the service
versiond add ~/projects # monitor a directory (baseline snapshot runs in the background)
versiond status
versiond history ~/projects/app/main.py
versiond diff ~/projects/app/main.py # last change
versiond restore '~/projects/app/src/*' --as-of 2026-10-08T14:00 # dry run
versiond restore '~/projects/app/src/*' --as-of 2026-10-08T14:00 --execute
```
API docs: <http://127.0.0.1:9922/docs>. API calls need `Authorization: Bearer $(versiond token)`.
Development: `python -m venv .venv && .venv/bin/pip install -e '.[dev]' && .venv/bin/pytest`.
---
## 1. Purpose
`versiond` is a per-user background service that **monitors directories of the user's choice** and records every version of the source and project files in them, stores them in a deduplicated, versioned archive on a remote WebDAV server, and exposes a local HTTP API for browsing history, diffing, restoring and purging.
@@ -68,9 +94,6 @@ ExecStart=%h/.local/bin/versiond serve
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=%h/.local/share/versiond %h/.config/versiond %h/.cache/versiond
MemoryMax=512M
Environment=PYTHONUNBUFFERED=1
@@ -78,7 +101,7 @@ Environment=PYTHONUNBUFFERED=1
WantedBy=default.target
```
Note: `ReadWritePaths` must be extended with the monitored roots (see §4.2), or `ProtectSystem`/`ProtectHome` hardening must be relaxed accordingly. Adding or removing a root through the API rewrites a drop-in (`versiond.service.d/roots.conf`) and restarts the unit.
Note: filesystem sandboxing (`ProtectSystem=`, `PrivateTmp=`, `ReadWritePaths=`) is deliberately left out. In a *user* unit these options need unprivileged user namespaces, which many distributions restrict (e.g. Ubuntu's AppArmor userns policy), so the unit would fail to start. Restores must also be able to write anywhere under the monitored roots. The service already runs unprivileged as the user, binds to loopback only, and refuses restore targets outside `$HOME` and the roots (§4.7).
### 2.3 Filesystem layout (XDG)
@@ -235,7 +258,7 @@ A file is rejected (HTTP `422` with a reason code for push requests; silently sk
- **Dependency, build and cache directories**, default list:
`node_modules`, `bower_components`, `jspm_packages`, `vendor`, `__pycache__`, `venv`, `env`, `site-packages`, `.tox`, `build`, `dist`, `target`, `out`, `bin`, `obj`, `.gradle`, `Pods`, `Carthage`, `DerivedData`, `.next`, `.nuxt`, `.svelte-kit`, `coverage`, `.terraform`, `_build`, `deps`, `elm-stuff`, `zig-cache`, `zig-out`.
- **Compiled/binary artifacts:** `*.pyc`, `*.pyo`, `*.class`, `*.o`, `*.obj`, `*.so`, `*.dylib`, `*.dll`, `*.exe`, `*.a`, `*.lib`, `*.wasm`, `*.jar`, `*.war`, `*.whl`, `*.egg`, archives, images, media, `*.lock` files above the size limit, `*.min.js`, `*.map`.
- **Binary content:** a file with NUL bytes in the first 8 KB, or not valid UTF-8/Latin-1, is rejected by default (`allow_binary = false`).
- **Binary content:** a file with a NUL byte in its first 8 KB is rejected.
- **User rules:** `.gitignore`-style patterns in `config.toml` (`[ignore] patterns = [...]`), plus optional respect of the project's own `.gitignore` (`respect_gitignore = true`, but `.env` is still captured unless explicitly excluded).
All filter decisions can be checked with `POST /filters/test` without storing anything.