Three-crate workspace per SPECS.md. varde-proto defines the full JSON Lines protocol (requests, envelopes, structured errors, events) with string-typed hashes so the crate carries no iroh dependency. The daemon binds its unix socket, loads config with flags > env > file > defaults precedence, and answers status/list; everything else returns a structured "unimplemented" error. varde-ctl maps subcommands 1:1 onto requests and round-trips status against a real daemon in the tests. Dependencies: serde/serde_json (wire format), tokio (async runtime and unix sockets), tracing/tracing-subscriber (structured logging), toml (config file), anyhow (binary-edge errors), thiserror (reserved for library errors), clap (ctl flag parsing, per spec), tempfile (dev-only, ephemeral test dirs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.6 KiB
varde — a system blob mirror daemon on iroh
Working name: varde (Norwegian: a stone cairn used as a waypoint/beacon). Rename freely; the crate prefix is used consistently below so a find-replace suffices.
This document is a build brief for Claude Code. Execute the milestones in order. Each milestone must compile, pass tests, and be committed before starting the next. Ask before deviating from the architecture; do not ask about implementation minutiae.
1. Vision
A small, boring, distro-packageable Linux daemon that owns a content-addressed blob store and mirrors content between consenting peers. Applications talk to it over a unix socket: "add this path", "pin this hash", "materialize hash X at path Y". The daemon handles storage, dedup, verification, LAN peer discovery, and replication policy. Think: what Windows Delivery Optimization is for updates, generalized, open, and built on iroh.
Design ethos (non-negotiable):
- Infrastructure, not product. No GUI, no self-updater, no bundled runtime. Small composable pieces with a stable socket interface, a man page, and systemd units.
- Legible to the network. The daemon never camouflages its traffic. Default posture is LAN-only with zero WAN upload. All background transfer is rate-limited and clearly attributable.
- Consent-tiered. Discovery is open; replication is explicit. Nothing is fetched or served without a standing policy the user created.
Non-goals (v1): gesture/DFT discovery (that arrives later as a separate discovery provider — see §6), WAN swarming, ISP policy channels (ALTO), Windows/macOS support, quotas/multi-tenant accounting.
2. Architecture
Cargo workspace, three crates:
varde/
├── varde-proto # API types: requests, responses, events. serde. No I/O.
├── varde-daemon # the service: store, endpoint, policy engine, socket server
└── varde-ctl # CLI client speaking the socket protocol (also serves as API reference impl)
Key dependencies and versions:
iroh-blobs = "0.35"— pin this. Per upstream, the post-0.35 rewrite is not yet production quality; 0.35 is the recommended production line. Use its persistent fs store (iroh_blobs::store::fs).iroh— matching version compatible with iroh-blobs 0.35.tokio,serde,serde_json,tracing,clap(ctl only),anyhow/thiserror.zbusfor NetworkManager metered-status (feature-gated,default-featureson Linux only).
Store layout: /var/lib/varde/ in system mode, $XDG_DATA_HOME/varde/ in user mode. Contains the iroh-blobs store directory, a meta.redb (or JSON for MVP) for pins/policies/trust, and the endpoint secret key (0600).
Materialization: when a client asks for a hash at a path, export from the store. Attempt reflink first (FICLONE ioctl via the reflink-copy crate) so btrfs/XFS users get zero-copy; fall back to copy. Never hardlink out of the store (store files must stay immutable). Document that the store dir and materialization targets should share a filesystem for reflink to work.
3. Socket API
JSON Lines over a unix stream socket. One request per line, one response per line, plus an event subscription mode. No varlink/D-Bus in v1 — keep the dependency surface minimal; the protocol is trivially wrappable later.
Socket paths: /run/varde/varde.sock (system), $XDG_RUNTIME_DIR/varde.sock (user). Support systemd socket activation (sd_notify + LISTEN_FDS) but also plain standalone bind for non-systemd distros.
Requests (define these as enums in varde-proto):
| Request | Semantics |
|---|---|
Add { path, recursive } |
Import file/dir into store. Returns root hash (HashSeq for dirs). |
Pin { hash, policy } |
Standing intent: keep this content, fetch if absent, serve to peers per policy. |
Unpin { hash } |
Remove intent. GC is a separate explicit op. |
Materialize { hash, dest, mode } |
Export to path. mode: reflink-or-copy | copy. |
Status { hash? } |
Global or per-hash: have/missing bytes, peers, transfer rates. |
List {} |
All pins with policies and completeness. |
TicketExport { hash } / TicketImport { ticket, pin_policy } |
iroh blob tickets — the v1 out-of-band sharing mechanism. |
Gc {} |
Drop unpinned blobs. |
Subscribe {} |
Switch connection to event stream (transfer progress, peer joined, pin complete). |
Every response carries {"ok": bool, ...}. Errors are structured (code, message), never bare strings.
4. Peer discovery and transfer (v1)
Two mechanisms only:
- Tickets — manual, explicit, works today.
varde ctl ticket export <hash>on machine A,ticket importon machine B. - LAN discovery — iroh's local discovery (mDNS-style). Advertise under a stable, documented service so the daemon is identifiable on the network, not anonymous noise. Peers on the same LAN that hold overlapping pins exchange content automatically.
Trust model v1: a peer allowlist keyed by iroh NodeId. varde ctl peer trust <node-id>. Content is only served to trusted peers and only fetched from trusted peers. An open-lan policy flag exists per-pin for deliberately public content but defaults off. All fetched data is BLAKE3-verified by iroh-blobs regardless — trust gates participation, not integrity.
Traffic posture:
- Token-bucket rate limiters on upload and download, configurable, with conservative defaults (e.g. 10 MB/s up on LAN, 0 on WAN).
- Set DSCP CS1/LE on the endpoint socket (best-effort; document that it may require
CAP_NET_ADMINor be silently ignored — that's fine). - LEDBAT is not available off-the-shelf in the Rust QUIC stack; do not attempt it in v1. The rate limiter + DSCP + LAN-only default is the v1 answer to "don't anger anyone." Leave a
transport.rsseam where scavenger congestion control could later slot in. - Metered awareness: query NetworkManager over D-Bus for the metered flag on the active connection. If metered: no background fetching, no serving, period. Feature-gated so the daemon still builds without D-Bus.
5. systemd and packaging
Provide in dist/:
varde.service+varde.socket(system) and user-unit variants. Hardening:DynamicUser=yesor dedicatedvardeuser,ProtectSystem=strict,StateDirectory=varde,RuntimeDirectory=varde,NoNewPrivileges=yes,RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK.- A man page
varde.8andvarde-ctl.1(scdoc or raw roff, your choice — scdoc preferred). - Example config
/etc/varde/config.toml: store path, socket path, rate limits, discovery on/off, WAN policy. - A
PKGBUILDskeleton for Arch (the author's daily driver) — best-effort, clearly marked untested.
Config precedence: CLI flags > env > config file > defaults. The daemon must run with an empty config.
6. Discovery provider seam (the redoal hook)
Define a trait in varde-daemon:
/// A source of content announcements the daemon may subscribe to.
/// v1 ships LanDiscovery + TicketImport. A future provider maps
/// gesture-derived gossip TopicIds (redoal) to announcement streams.
trait DiscoveryProvider {
fn subscribe(&self, topic: TopicKey) -> BoxStream<Announcement>;
fn announce(&self, topic: TopicKey, ann: Announcement) -> Result<()>;
}
struct Announcement {
root: Hash, // HashSeq root
author: PublicKey, // ed25519, signs the announcement
meta: BTreeMap<String, String>,
providers: Vec<NodeAddr>,
}
Announcements are signed; the daemon indexes all announcements on subscribed topics but only auto-fetches from trusted author keys. This is the consent tiering: discovery open, replication explicit. Do not implement a gossip provider in v1 — just make LanDiscovery conform to the trait so the seam is proven.
7. Milestones
Each milestone = compiling code + tests + a short docs/milestone-N.md note on decisions made.
- Skeleton. Workspace,
varde-prototypes, daemon binds socket,varde-ctl statusround-trips against an empty daemon. Config loading. Tracing setup. - Store. iroh-blobs 0.35 fs store wired in.
Add,List,Materialize(reflink + fallback),Gc. Integration test: add a directory tree, materialize it elsewhere, byte-identical, reflink verified on btrfs (skip-if-unsupported in CI). - Transfer. iroh endpoint, ticket export/import,
Pintriggers fetch from ticket-embedded provider. Two-daemon integration test over localhost. - LAN + trust. Local discovery, peer allowlist, auto-sync of overlapping pins between two trusted daemons. Rate limiting. Event subscription streaming progress.
- Citizenship. Metered detection, DSCP attempt, systemd units + socket activation, man pages, config example, hardening pass,
varde-ctlpolish (human +--jsonoutput). - Seam proof.
DiscoveryProvidertrait, LanDiscovery refactored onto it, signedAnnouncementtype with verification tests. Writedocs/redoal-integration.mdsketching the gesture-topic provider without implementing it.
8. Testing and quality bar
cargo clippy -- -D warnings,cargo fmt --checkin CI (a simple Woodpecker/Gitea Actions yaml in.ci/).- Integration tests spawn real daemons on ephemeral sockets/ports; no mocking iroh.
- Property test: any file round-trips add→materialize bit-identically (proptest, bounded sizes).
- The daemon must survive
kill -9mid-transfer and resume cleanly on restart (iroh-blobs handles partial state; write a test proving it).
9. Style
Plain Rust, minimal generics, no async-trait acrobatics where a concrete enum suffices. Errors: thiserror in libraries, anyhow at binary edges. Every public item in varde-proto documented. Comments explain why, not what. No dependency added without a one-line justification in the commit message.