Files
varde/SPECS.md
T
bl 4e1a95613b Milestone 1: workspace skeleton, wire protocol, socket round-trip
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>
2026-07-14 20:10:29 +02:00

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.
  • zbus for NetworkManager metered-status (feature-gated, default-features on 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:

  1. Tickets — manual, explicit, works today. varde ctl ticket export <hash> on machine A, ticket import on machine B.
  2. 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_ADMIN or 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.rs seam 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=yes or dedicated varde user, ProtectSystem=strict, StateDirectory=varde, RuntimeDirectory=varde, NoNewPrivileges=yes, RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK.
  • A man page varde.8 and varde-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 PKGBUILD skeleton 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.

  1. Skeleton. Workspace, varde-proto types, daemon binds socket, varde-ctl status round-trips against an empty daemon. Config loading. Tracing setup.
  2. 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).
  3. Transfer. iroh endpoint, ticket export/import, Pin triggers fetch from ticket-embedded provider. Two-daemon integration test over localhost.
  4. LAN + trust. Local discovery, peer allowlist, auto-sync of overlapping pins between two trusted daemons. Rate limiting. Event subscription streaming progress.
  5. Citizenship. Metered detection, DSCP attempt, systemd units + socket activation, man pages, config example, hardening pass, varde-ctl polish (human + --json output).
  6. Seam proof. DiscoveryProvider trait, LanDiscovery refactored onto it, signed Announcement type with verification tests. Write docs/redoal-integration.md sketching the gesture-topic provider without implementing it.

8. Testing and quality bar

  • cargo clippy -- -D warnings, cargo fmt --check in 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 -9 mid-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.