diff --git a/README.md b/README.md index 71ee811..37f2442 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,45 @@ # Appa -Appa is a peer-to-peer folder synchronization utility built in Rust on iroh. -It watches local folders and syncs changes directly between your devices, with -no account, central server, or web app. +Appa keeps folders in sync directly between your devices. It has no account and +no central file server. Each device runs one local daemon; the command line and +COSMIC app talk to that daemon. -See [the architecture reference](docs/architecture.md) for Appa's folder, -device, member, peer, node, and capability terminology. +## Start the daemon -## Run Appa as a service - -After installation, Appa runs as one long-lived local daemon that owns the -iroh node, local state, file watchers, and sync loop. The CLI and COSMIC app -connect to it as clients. - -Install the per-user service once +Install the per-user service once: ```sh appa service install ``` -It starts immediately and starts again at login. The service uses systemd on -Linux and launchd on macOS. Check it with `appa service status`; `logs`, -`restart`, and `uninstall` are available through `appa service` too. +It starts now and at login. Use `appa service status`, `restart`, `logs`, or +`uninstall` to manage it. -For development, debugging, or another process manager, run the daemon in the -foreground +For development or another process manager, run it in the foreground: ```sh appa daemon serve ``` -`appa run` is also a foreground daemon. Pass a folder to sync only that -folder; `appa daemon serve` always syncs every registered folder. Stop either -form with Ctrl-C. - -## What is different - -There is no Appa account or central Appa server. Each device has its own -cryptographic identity. An invitation gives a device the secret and signed -membership record for one folder. Devices connect directly when possible and -use an Iroh relay when a direct route is unavailable. - -Appa stores file data as content-addressed blobs and compares compact Merkle -summaries before moving it. In the common case it transfers only the changed -paths and missing file data. If two devices edit the same file while apart, -Appa keeps both copies instead of silently choosing one. - -## Install - -Appa requires Rust 1.97.1 or newer. From the repository root - -```sh -cargo install --path . -``` - -Or build it without installing +## Share a folder -```sh -cargo build -./target/debug/appa --help -``` - -With Nix - -```sh -nix develop -nix build .# -``` - -## Sync a folder - -On the first device +On the first device: ```sh appa init ~/notes appa invite ~/notes --copy ``` -Send the invitation to your other device. It is reusable for 24 hours with no -use limit, so treat it like a password. - -On the other device +On another device, create the destination directory and join with the +invitation: ```sh appa join ~/notes --stdin ``` -Confirm the join, paste the invitation, then press Ctrl-D. The local daemon -now watches the folder and syncs changes whenever both devices are reachable. +An invitation grants access to one folder. Treat it like a password. -Check the connection from either device +Check a folder at any time: ```sh appa status ~/notes @@ -97,190 +47,107 @@ appa peers ~/notes appa members ~/notes ``` -Run `appa status --watch` for a live view. - -## Common commands - -| Task | Command | -| ---------------------------- | ---------------------------------- | -| Add a folder | `appa init ` | -| Invite a device | `appa invite --copy` | -| Join a folder | `appa join --stdin` | -| Run the daemon in foreground | `appa daemon serve` | -| Check sync health | `appa status [folder]` | -| List devices | `appa members ` | -| List peer routes | `appa peers ` | -| List conflicts | `appa conflicts ` | -| Show revision history | `appa history ` | -| Restore a revision | `appa restore ` | -| Stop syncing a folder | `appa leave ` | -| Remove a device | `appa revoke ` | - -`status`, `peers`, and `doctor` support `--json`. Run -`appa --help` for every option. - -## Ignore local files - -Add a `.appaignore` file to the root of a synced folder. It uses gitignore -syntax and stays local. - -```gitignore -private/ -*.tmp -.DS_Store -``` - -Symbolic links are ignored: they are not synced and produce no error. -Non-Unicode filenames are rejected rather than lossily renamed during -synchronization. +`appa status --watch` refreshes the display once a second. -## Import local folders +## Everyday commands -`appa folder` manages a non-secret local folder inventory. It is useful for -provisioning or recreating local folder paths and modes. It does not contain -invitations, folder IDs, capabilities, membership, or any other shared-folder -state. Join shared folders with an invitation instead. +| Task | Command | +| --- | --- | +| Add a local folder | `appa init ` | +| Invite a device | `appa invite --copy` | +| Join a shared folder | `appa join --stdin` | +| View health | `appa status [folder]` | +| View conflicts | `appa conflicts ` | +| View saved revisions | `appa history ` | +| Restore a revision | `appa restore ` | +| Verify the audit log | `appa audit --verify` | +| Remove local Appa state | `appa forget ` | -Write an inventory from your current folders +`forget` leaves files alone. It does not revoke the device from the shared +folder. Only the folder owner can revoke a member: ```sh -appa folder template +appa revoke ~/notes ``` -This writes `appa.toml` by default with one `[[folders]]` entry per folder. -Each entry has a `path`, optional `name`, and local `mode`. `mode` is -`send_receive` by default, `send_only`, or `receive_only`. +Revocation rotates the folder capability. Create new invitations for members +who should retain access. -Validate and import +## Local folder inventory + +Appa can export and import a small, non-secret list of local folders. It is for +setting up local paths and sync modes in bulk, not for sharing folders. ```sh +appa folder template appa folder validate appa folder import ``` -`import` registers new folders and updates the local mode of existing ones. It -uses the daemon, so it is safe to run while Appa is active. It does not remove -folders that are absent from the inventory. - -## Change folders while Appa runs - -The daemon refreshes its folder list and settings every second. Run `appa -init`, `appa join`, or `appa leave` from another terminal and it starts -watching added folders, stops watching removed folders, and reloads changed -folder settings without a daemon restart. - -## Handle conflicts - -When two devices edit the same file offline, Appa keeps both versions. The -extra copy gets a name like `notes (conflict-device-content).txt`. - -```sh -appa conflicts ~/notes -``` - -Compare the files, keep the content you want, then delete the conflict copy. +The default file is `appa.toml`. Entries contain a path, optional display name, +and local mode. They do not contain invitations, capabilities, identities, or +membership. `folder import` is additive: it registers missing folders and +updates local modes, but never removes a folder absent from the file. -## Restore files +## Files and conflicts -Appa keeps the latest 100 folder revisions by default. +Add `.appaignore` at a folder root to keep matching paths local. It uses +gitignore syntax. Symlinks are ignored and non-Unicode filenames are rejected. -```sh -appa history ~/notes -appa restore ~/notes -``` +When devices edit the same file while disconnected, Appa keeps both versions. +Use `appa conflicts ` to find the additional copies, then choose the +content you want. -Set `APPA_HISTORY_REVISIONS` to a positive integer to change the limit. This -controls revision history, not Iroh's blob storage. +Appa keeps 100 local revisions by default. Set `APPA_HISTORY_REVISIONS` to a +positive value to change that limit. -## Manage devices +## Identity and diagnostics -Each Appa installation has a device identity. You can back it up outside your -synced folders +Each installation has a device identity. Back it up outside synchronized +folders: ```sh appa identity appa identity export ~/appa-identity-backup ``` -The backup can act as that device. Keep it private. Restore it on a replacement -device with +The backup can act as the device. Keep it private. Stop the daemon before +importing it on a replacement device: ```sh appa identity import ~/appa-identity-backup ``` -The device that initializes a folder owns it. Only the owner can remove another -device - -```sh -appa members ~/notes -appa revoke ~/notes -``` - -Revoking a device rotates the folder secret. Send fresh invitations to the -devices that should keep access. - -To stop syncing locally without deleting any files +For problems, start with: ```sh -appa leave ~/notes +appa doctor ``` -## Finding peers - -Appa uses Iroh for encrypted direct connections when possible and relay routes -when necessary. On a shared LAN, it also uses mDNS to discover a peer's direct -address. mDNS only finds a route. It does not make a folder public or replace -the invitation, roster, or encryption checks. +Set `RUST_LOG=appa=debug` for detailed logs. `APPA_HOME` changes the location +of Appa's local state. -## COSMIC desktop companion +## Build -`appa-cosmic` is a native libcosmic companion for Linux. It shows folder -health, diagnostics, conflicts, and members; it can also add or join folders, -create invitations, revoke a member, and install the background service. - -With Nix +Appa requires Rust 1.97.1 or newer. ```sh -nix develop -c cargo run -p appa-cosmic -``` - -The GUI talks to the long-lived local Appa daemon. Start it directly while -developing, or install the user service from the GUI - -```bash -nix develop -c cargo run -- daemon serve +cargo install --path . ``` -Build the packaged application with `nix build .#appa-cosmic`. - -## Troubleshoot +With Nix: ```sh -appa status -appa doctor +nix develop -c cargo build +nix build .# ``` -`status` shows sync times, peers, conflicts, and the latest error. `doctor` -checks identities, folders, manifests, membership, and recorded sync failures. - -Useful environment variables - -- `APPA_HOME` sets the directory for Appa's identity and local state. It defaults to `~/.local/share/Appa` on Linux, `~/Library/Application Support/Appa` on macOS, and `%LOCALAPPDATA%\Appa` on Windows. -- `APPA_HISTORY_REVISIONS` sets the number of saved folder revisions. -- `RUST_LOG=appa=debug` enables detailed logs. - -Appa keeps syncing with healthy peers when another peer is offline. Failed -connections retry automatically. - -## Shell completions +The optional COSMIC companion is built with: ```sh -appa completions zsh > ~/.zfunc/_appa +nix develop -c cargo run -p appa-cosmic ``` -Supported shells are Bash, Elvish, Fish, PowerShell, and Zsh. - ## License -MIT or Apache 2.0. +MIT or Apache-2.0. diff --git a/docs/architecture.md b/docs/architecture.md index 8cac475..97e6abb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,98 +1,74 @@ # Appa architecture -Appa is a local-first folder synchronizer. One daemon owns the local node, -database, watchers, and sync work. The CLI and COSMIC app send requests to it -over a socket owned by the current user. This keeps the network state in one -place and keeps ordinary commands from starting another synchronizer. - -## Terms - -- **Folder** is one local directory registered for synchronization. It has a - stable ID, a capability, a mode, a signed roster, and a manifest. -- **Device** is an Appa installation identified by its Iroh public key. The key - is stored in Appa state and is not derived from a hostname. -- **Member** is a device in a folder's signed roster. Members have owner, - read/write, or read-only rights. -- **Peer** is a route to a member device. It can be a direct LAN or WAN route, - or an Iroh relay route. -- **Node** is the daemon's Iroh endpoint. It hosts folders and moves blobs. - `appa daemon serve` creates one node for all configured folders. -- **Capability** is the secret required to request a folder's control data. An - invitation carries it with a signed roster. Treat it like a password. - -## Sync flow - -1. A local scan builds a manifest from the folder and imports changed files as - Iroh blobs. -2. The node advertises its manifest root to known peers. Announcements are an - optimization; periodic peer checks still converge after missed events. -3. Peers compare Merkle nodes and request only changed manifest entries. -4. Reconciliation applies newer versions, preserves concurrent edits as - conflict copies, and propagates tombstones for deletions. -5. Changed blobs are downloaded before filesystem materialization. Appa saves - a pending materialization record first, so an interrupted write can be - recovered on the next sync. - -## Audit log - -Appa keeps a separate signed audit log for each folder. The compact manifest -history supports local restore and is allowed to prune old revisions. The audit -log is append-only and retained independently. - -Each device signs its own sequence of events. Every event names its parent -hash, the manifest root, and the roster hash that was current when it was -created. Devices exchange missing events through the authenticated control -protocol in bounded batches. A device that has retained a newer signed head can -detect a later rollback, omission, altered event, or conflicting event at the -same author sequence. - -This is not a global blockchain or a replacement for Appa's vector clocks. -Vector clocks continue to resolve file causality. Merkle trees continue to make -manifest comparison efficient. The audit log records who committed a state and -which prior event they extended. - -Audit evidence begins when an honest member has observed and retained a head. -It cannot prove that a first-seen state was not fabricated, protect against a -stolen signing key, or prove facts that no folder member retained. - -All control streams and blob transfers are authenticated and encrypted by -Iroh. The daemon uses mDNS on a LAN to find direct routes. Iroh uses relay -routes when no direct route works. Discovery only finds routes. It does not -change encryption or membership checks. - -## Local state and configuration - -Appa state contains the device identity, SQLite state database, blob cache, -and process locks. `APPA_HOME` overrides its location. `appa.toml` is optional -and declarative. It describes folders and reads capabilities from environment -variables, but it never stores a capability itself. - -The state directory is intentionally separate from synchronized folders. To -remove a folder's synchronization state without touching its files, use -`appa forget `. This is a local operation and does not alter remote -membership. - -## Version boundaries - -Appa has four deliberately independent version signals. They protect different -persisted or networked formats and should only change with their corresponding -format. - -- The Iroh control-stream ALPN is `appa/sync/4`. It selects a wire-compatible - control protocol before either peer processes a request. -- The invitation protocol version is `5`. It covers the signed invitation - payload and its validation rules. -- The folder protocol version is `2`. It covers the signed folder roster and - manifest model. -- The declarative configuration version is `1`. It covers `appa.toml` only. - -Local SQLite state has no migration ladder by design. Appa is currently a v1 -project and intentionally starts from fresh local state instead of carrying -compatibility code for pre-release formats. - -## Filesystem rules - -Appa ignores paths matched by `.appaignore` and skips symbolic links. Manifest -paths must be Unicode; a non-Unicode filename is rejected during scanning -instead of being lossy-converted into a different path. This is a deliberate -safety boundary while Appa's cross-platform manifest format uses text paths. +Appa is local-first folder synchronization. One daemon owns the local Iroh +endpoint, SQLite state, file watchers, and synchronization loop. Clients use a +user-owned local socket so they do not create competing synchronizers. + +## Model + +- A **folder** is a local directory with a stable ID, local mode, manifest, + capability, and signed roster. +- A **device** is an Appa installation identified by its Iroh public key. +- A **member** is a device named in a folder roster. Members may own, write, + or read a folder. +- A **capability** is the folder secret carried by invitations and control + requests. It is not stored in folder-inventory files. +- A **peer** is a direct or relay route to a member device. + +The daemon discovers LAN routes with mDNS and can use Iroh relays when a direct +route is unavailable. Discovery supplies routes only; membership and encrypted +connections remain enforced by Iroh and Appa. + +## Synchronization + +1. A scan builds a manifest and imports changed file data into the blob store. +2. Peers compare manifest Merkle trees and exchange only changed entries. +3. Version clocks decide whether a change is newer or concurrent. +4. Concurrent edits become conflict copies rather than silently overwriting a + file. +5. Missing blobs are fetched before filesystem materialization. A pending + materialization record permits recovery after interruption. + +Announcements improve latency. Periodic peer checks still converge when an +announcement is missed. + +## Audit evidence + +Folder history and audit evidence have different jobs. Local revision history +is compact and bounded for restore. The audit log is append-only and retained +independently. + +Every device has a signed chain of audit events. Each event includes its parent +hash, manifest root, and roster hash. Peers exchange missing events through the +authenticated control protocol. A device that retained a newer head can detect +a later rollback, altered event, omission, or conflicting event at the same +author sequence. + +This does not make Appa a blockchain. Version clocks handle file causality; +Merkle trees make manifest comparison efficient; audit chains record signed +state transitions. + +The guarantee starts once an honest member has retained a head. A stolen device +key can still sign events, and no participant can prove facts that no honest +member observed. + +## Local state + +Appa state includes the device identity, SQLite database, blob cache, and +daemon socket. `APPA_HOME` changes its location. It is separate from synced +folders. + +`appa forget ` removes local Appa state without touching files or +remote membership. Folder inventories are optional local input files for bulk +registration and mode changes. Invitations remain the only way to join an +existing shared folder. + +## Format boundaries + +- `appa/sync/4` is the authenticated Iroh control protocol. +- Invitation format version `5` covers signed invitation payloads. +- Folder format version `2` covers rosters and manifests. +- Folder inventory format version `1` covers `appa.toml`. + +SQLite state has a migration ladder. Network and signed-format versions change +only when their corresponding representation becomes incompatible.