Commit Graph

3214 Commits

Author SHA1 Message Date
Maciek "mab122" Bator 76e3d6ced8 docs: note package/publish gaps blocking the 10MB-fix release
Code fix (35f1f972) is done, tested, and pushed. Package build + Forgejo
package-registry publish is blocked on missing packaging conventions
here (no existing publish script/credentials/target-scope decision) --
see NOTES-package-publish-todo.md for what's needed to finish this.
2026-07-19 12:51:54 +02:00
Maciek "mab122" Bator 35f1f97290 Fix: rad lfs fetch fails on any LFS object over 10MB from IPFS
`ipfs::cat()` used ureq's `Body::read_to_vec()`, which defaults to a
10MB response-size limit meant for typical HTTP API responses. Any LFS
object larger than that (e.g. the ~29MB thumbnail.xcf in the blog repo)
failed with "the response body is larger than request limit: 10485760",
breaking `rad lfs fetch`/`rad lfs store`-driven smudges for large files.

Raise the cap to 1GiB via ureq's `with_config().limit(...)`, since LFS
content is exactly the out-of-band-large-file case this whole feature
exists for. Verified: reproduced the exact production error with a 15MB
round-trip test against a live Kubo daemon, confirmed it fails without
the fix and passes with it, and confirmed live against the actual
29MB thumbnail.xcf object that broke deploy.
2026-07-19 12:49:41 +02:00
Maciek "mab122" Bator 4ae9756a3d docs: explain the fetch-batch worker in LFS-IPFS.md 2026-07-17 23:43:18 +02:00
Maciek "mab122" Bator a8c8dbf3dd Bump radicle-lfs-transfer submodule: batched multi-object fetch worker 2026-07-17 23:42:40 +02:00
Maciek "mab122" Bator 7c6d342ef9 lfs: add fetch-batch, a long-lived worker for multi-object downloads
`rad lfs fetch` still exists for single-object/manual use, but
`radicle-lfs-transfer` used to spawn a fresh one per object during
`git lfs pull`/checkout -- for a private repo, each one independently
loaded the signer, so N files meant N passphrase prompts. Same bug
class `rad lfs precommit` already fixed for commits, unfixed here
until now.

Unlike precommit (which knows every staged file upfront and can read
stdin until EOF), fetch can't collect a batch before starting:
git-lfs's custom-transfer protocol requests objects one at a time.
`rad lfs fetch-batch` instead stays alive for as long as its caller
keeps its stdin open, answering one "<oid> <size> <out-path>" request
at a time with a JSON response line, reusing the same unwrapped
signer across all of them -- paired with radicle-lfs-transfer's new
FetchWorker, which spawns this once per transfer session instead of
once per object.

Also factors `fetch::run`'s core logic out into `fetch_object`,
taking a `&mut Option<MemorySigner>` the same way `store::store_object`
already does, so both the single-shot `rad lfs fetch` and the new
batch worker share one implementation.
2026-07-17 23:42:30 +02:00
Maciek "mab122" Bator 4ab6fc2499 docs: document rad lfs backfill and the unseed refcounting fix 2026-07-17 15:22:31 +02:00
Maciek "mab122" Bator c059c7967b lfs: cross-repo pin refcounting on unseed, add `rad lfs backfill`
Two known gaps, both flagged as follow-up work in LFS-IPFS.md:

`rad unseed` unconditionally unpinned every CID a repository's LFS
notes referenced. Two repositories that happen to track
byte-identical file content get the same content-addressed CID, so
unseeding one could silently evict content another still-seeded
repository needed -- that repo's next `git lfs fetch` would then fail
with no obvious cause. Fixed by checking every other currently-seeded
repository's own LFS notes before unpinning, and skipping any CID
still referenced elsewhere. No persistent refcount is kept anywhere;
this recomputes the still-needed set from git-notes each time,
consistent with the rest of this design treating notes as the single
source of truth rather than maintaining separate bookkeeping that
could drift out of sync.

`rad lfs backfill` retroactively pins any LFS-tracked file at HEAD
that doesn't yet have a note -- the situation a `--no-verify` commit
or a commit made while `ipfs`/`rad` weren't on `PATH` leaves behind
(the pre-commit hook soft-skips pinning in the latter case rather
than blocking the commit entirely). Content is read from Git LFS's
own local object cache (populated by the clean filter on `git add`
regardless of whether the hook ran), falling back to the working-tree
file. Reports what it backfilled, what already had a note, and what
it couldn't find content for locally (meaning some other peer that
does have it needs to run this instead).
2026-07-17 15:21:40 +02:00
Maciek "mab122" Bator 581843fa92 build: rerun-if-changed on .git/logs/HEAD, fix stale version on rebuild
Without an explicit rerun-if-changed directive, cargo has no way to
know it needs to re-run this build script just because the current
commit changed -- by default it only reruns based on files inside
the crate's own source tree, and .git is entirely outside that. This
let GIT_HEAD/RADICLE_VERSION silently go stale: several `cargo
build`s in a row during this session kept reporting an old commit's
version despite the working tree being clean at a newer commit,
because cargo decided nothing needed rebuilding at all.

`.git/logs/HEAD` (the reflog) is touched on every commit, checkout,
and merge, unlike `.git/HEAD` itself (which only changes when
switching branches) -- watching it catches the case that actually
matters here.
2026-07-17 09:42:30 +02:00
Maciek "mab122" Bator 2306aabc4a docs: add fork banner to .github/README.md (GitHub profile-README convention)
Matches the banner already on the root README.md -- this file is
otherwise upstream heartwood's own GitHub-specific profile readme,
unrelated to and predating the LFS-over-IPFS work, so someone landing
on it directly (or via GitHub's own rendering convention) had no
indication this repo is a fork with different setup instructions.
2026-07-17 09:33:39 +02:00
Maciek "mab122" Bator 3ad836577a Bump radicle-lfs-transfer submodule: doc update for push refspec fix 2026-07-16 15:23:22 +02:00
Maciek "mab122" Bator a84aeb23f7 lfs: configure a branch push refspec too, single `git push rad` now works
`rad lfs init` previously only configured a push refspec for the
notes ref (`+refs/notes/rad-lfs/local:refs/notes/rad-lfs`) -- once
any explicit push refspec exists on a remote, git stops falling back
to push.default-driven behavior for a bare `git push rad`, so it only
ever pushed the notes mapping, never the branch. That's the exact
footgun that just caused a real "blog did not update" report: `git
push rad` (bare) after a commit only sent the notes ref, silently
leaving the commit itself unpushed.

Add a second push refspec, `+refs/heads/*:refs/heads/*`, mirroring
the existing `+refs/heads/*:refs/remotes/rad/*` fetch refspec already
configured. A bare `git push rad` now pushes every local branch and
the notes mapping together in one command.

Only takes effect once `rad lfs init` has been (re-)run with this
build -- it's an additive, idempotent config change via the existing
`ensure_refspec` helper, no migration/cleanup needed since there's no
stale conflicting entry to remove this time. Updated `rad lfs init`'s
own success/info messages plus README.md/LFS-IPFS.md/
radicle-lfs-transfer's README to match -- the "push gotcha" workaround
they documented is now the *old*-repository fallback path, not the
primary instructions.
2026-07-16 15:23:00 +02:00
Maciek "mab122" Bator 245fdd3fdc Bump radicle-lfs-transfer submodule: fix stale pre-batching README claim 2026-07-16 14:39:30 +02:00
Maciek "mab122" Bator 5227783cac docs: fix nonexistent rad push/pull commands, stale pre-batching claims
There is no `rad push`/`rad pull` subcommand -- `rad`'s actual
subcommand set is auth/checkout/clone/.../sync/watch (see `rad
--help`); pushing/pulling git refs (branch, notes) is done via plain
`git push rad`/`git pull rad`, same as any git remote. LFS-IPFS.md's
"Use" section and `rad lfs init`'s own printed success message both
said `rad push`/`rad pull`, which doesn't exist and doesn't even
gesture at the real push gotcha (branch and notes ref need two
separate pushes).

Also updates LFS-IPFS.md's "How it works" section, which still
described the pre-batching design (a pre-commit hook calling `rad lfs
store` once per file, and the bare `refs/notes/rad-lfs` as if it were
the local write target) -- both stale since the LOCAL_NOTES_REF fix
and the `rad lfs precommit` batching added this session. `rad lfs
init`'s own success message now proactively states the push gotcha,
rather than only documenting it after the fact in Troubleshooting.
2026-07-16 14:39:03 +02:00
Maciek "mab122" Bator 891383bf7b scripts: skip already-published versions, attach a changelog
publish-package.sh now checks the Forgejo package listing first and
exits early if the current version is already published, instead of
failing with a 409 on the first upload attempt. Also generates a
changelog (git log since whichever previous package's embedded
short-sha we can find) and attaches it both inside the tarball
(MANIFEST.txt, CHANGELOG.txt) and as its own uploaded file, so anyone
browsing the package page can see what changed without downloading
the archive.
2026-07-16 14:07:14 +02:00
Maciek "mab122" Bator 68202ce58d build: tag rad --version with a +lfs-ipfs suffix, fix newline bug
Appends a semver build-metadata suffix to RADICLE_VERSION so `rad
--version` (and radicle-node/git-remote-rad, which share this build
script via symlink) unambiguously identify a binary as this fork
rather than upstream heartwood -- useful for checking what a deploy
target is actually running.

While wiring this up, found and fixed a real pre-existing bug: none
of the three `git` subprocess outputs captured here (GIT_HEAD,
RADICLE_VERSION, SOURCE_DATE_EPOCH) were trimmed, so each carried a
trailing newline into its `cargo::rustc-env=KEY=VALUE` directive.
Harmless on its own since nothing followed it, but appending the
+lfs-ipfs suffix landed after that embedded newline, splitting the
directive across two lines -- cargo silently discards the second
line as unrecognized, so the suffix never actually took effect until
this trim was added.

Also adds scripts/publish-package.sh: builds all four release
binaries (rad, radicle-node, git-remote-rad, rad-lfs-transfer) and
publishes them as a Forgejo generic package, so a deploy target
without a Rust toolchain can download instead of rebuilding from
source (see NOTES-lfs-store-note-write-bug.md's VPS deployment story
for why that matters).
2026-07-16 13:49:13 +02:00
Maciek "mab122" Bator ae7aed5db5 docs: add quickstart cheatsheet to README, clarify push gotcha
Adds a copy-pasteable day-to-day cheatsheet (init, track/commit/push,
clone-elsewhere, private-repo rekey) right after the existing "Zero to
usable" build section. Also corrects/expands the existing
git-push-rad troubleshooting row in LFS-IPFS.md based on today's live
round-trip test: `rad lfs init` only configures a push refspec for the
notes ref, not the branch, so pushing a commit needs both
`git push rad <branch>` and a separate bare `git push rad` -- missing
the second step is what produces "no CID recorded for oid" on a
collaborator's clone.
2026-07-16 12:54:42 +02:00
Maciek "mab122" Bator fa4fdf5470 lfs: fix pre-commit hook silently skipping non-ASCII filenames
`git diff --cached --name-only` C-quotes filenames with non-ASCII
bytes by default (e.g. a literal "\305\202" for a Polish "ł"), which
doesn't match any real path on disk -- the hook's `[ -f "$file" ]`
check silently skipped those files instead of erroring, so they were
committed as valid LFS pointers but never pinned to IPFS. Use `-z`
(NUL-separated, unquoted) and convert to newlines for the read loop.

Also add an idempotency fast-path to `rad lfs store`/`store_object`:
if a note already exists for an object (e.g. written moments earlier
by `rad lfs precommit`), return its CID directly instead of
re-encrypting. This avoids redundant work, and matters more than that
for private repos specifically: `git push`'s custom-transfer-agent
subprocess chain has no TTY, so without this fast path every push
would fail outright on the passphrase prompt even for objects already
stored interactively at commit time.
2026-07-16 12:41:01 +02:00
Maciek "mab122" Bator c070aac6fe lfs: batch pre-commit hook into one `rad lfs precommit` process
Previously the pre-commit hook shelled out to `rad lfs store` once per
staged LFS file, each a separate process. For private repos, each of
those needs a keystore passphrase for an ECDH key-agreement operation,
so a multi-file commit meant a separate interactive passphrase prompt
per file -- observed live against the blog repo (9+ prompts in a row).

Add `rad lfs precommit`, which reads every staged file's oid/size/path
from stdin and processes them all in one process, loading the signer
at most once and reusing it across the batch. `store::run` now
delegates to a shared `store_object` helper that both it and
`precommit::run` call.
2026-07-15 19:23:21 +02:00
Maciek "mab122" Bator a9428a9ef9 Fix: rad lfs store/rekey fail with git ref D/F conflict on notes ref
Found live while migrating the blog's food-gallery images into LFS,
diagnosed in NOTES-lfs-store-note-write-bug.md, confirmed and fixed here.

Root cause: store.rs/rekey.rs wrote local notes to the bare
refs/notes/rad-lfs, but the earlier fetch-bug fix (1c65ee62) has the
fetch refspec populate refs/notes/rad-lfs/<peer> siblings locally --
including your own peer's copy, fetched back after your own push. A bare
refs/notes/rad-lfs ref can't coexist with refs/notes/rad-lfs/<peer> in
the same git ref namespace (a leaf ref vs. a directory prefix at the same
path) -- so the very first commit+push+fetch cycle on a repo left it
permanently unable to `rad lfs store` again, failing with "failed to
write LFS note" and no further detail (git itself hits the same conflict
from the fetch direction and just refuses that one ref cleanly; it's
specifically libgit2's ref-write path used here that fails hard instead).

Fix: stop writing to the bare ref locally at all. New
lfs_crypto::LOCAL_NOTES_REF (refs/notes/rad-lfs/local) is what
store.rs/rekey.rs write to now -- living under the same
refs/notes/rad-lfs/ prefix as every fetched peer ref makes it just
another sibling leaf ref, structurally incapable of conflicting with
them ("local" can never collide with an actual peer ID). NOTES_REF
(bare) stays exactly as it was for the remote-side/push-destination
role, unchanged. rad lfs init's push refspec becomes asymmetric
(+refs/notes/rad-lfs/local:refs/notes/rad-lfs) instead of symmetric;
fetch refspec is untouched.

Added a migration step (migrate_local_notes_ref, run by rad lfs init)
for already-affected repos: reads any existing bare-ref notes out,
deletes the bare ref, then re-writes them under the new local ref --
also removes the old symmetric push refspec config entry the same way
the earlier fetch-refspec migration did, so re-running rad lfs init
fully repairs an already-broken repo with no manual git surgery needed.
(First version of the migration itself had the identical bug -- tried to
write the new ref before deleting the old one, hit the same conflict
self-inflicted -- fixed by reading all notes into memory before deleting
the bare ref.)

Verified locally: reproduced the exact failure in the precise order that
triggers it, confirmed the fix resolves that exact same failing call in
the same repo, verified the migration path preserves note content
exactly, and confirmed a full commit->push->fetch->decrypt round trip
plus `rad lfs rekey` all still work correctly afterward.
2026-07-15 19:02:53 +02:00
Maciek "mab122" Bator 945da6f7f8 docs: record root cause and live verification of Bug #2
Bug #2 (ls-remote vs fetch disagreeing on the VPS) turned out to be two
separate things, neither a bug in the fetch fix itself: a stale,
unrelated system-wide git-remote-rad binary being silently used in
ad-hoc manual testing (PATH had no ~/.radicle/bin), plus a genuine
cross-device rename bug in rad-lfs-transfer's download path (fixed
separately, ac4a27c) that only surfaced once the real pipeline actually
ran with the fetch fix for the first time. Verified live: triggered
blog-deploy.service for real, full success end-to-end, SHA-256-verified.
2026-07-14 18:53:45 +02:00
Maciek "mab122" Bator a97484c92f Bump radicle-lfs-transfer submodule: fix cross-device rename on LFS download 2026-07-14 18:44:06 +02:00
Maciek "mab122" Bator ff762ef8f2 Prompt interactively for passphrase instead of requiring RAD_PASSPHRASE
Private-repo LFS operations need a signer capable of ECDH, which
ssh-agent (the normal day-to-day flow after `rad auth`) can't provide --
that's a hard protocol limitation (standard ssh-agent only implements
signing, there's no key-agreement request type), not a gap in our code,
so there's no way to make it work "through the agent" the way signing
does.

Previously the only alternative to an unencrypted keystore was setting
RAD_PASSPHRASE, which breaks the normal ssh-agent-based workflow and
means putting a password in an environment variable -- not ergonomic,
and not how any other `rad` command behaves.

`rad` already has an established fallback for exactly this situation:
`crate::terminal::io::signer` tries ssh-agent first, and if that's
unavailable/unregistered and we're connected to a TTY, prompts
interactively for the passphrase via `inquire` (reusing the existing
`PassphraseValidator`). `lfs_crypto::load_signer` was missing this last
step entirely -- it only tried "unencrypted keystore" then
"RAD_PASSPHRASE" then gave up. Added the same interactive-prompt
fallback, reusing the exact same validator/prompt helpers already used
elsewhere, before falling back to a hard failure.

This covers the common case (an interactive `git commit` triggering the
pre-commit hook, which inherits the terminal's TTY) with no environment
variable needed at all. Only genuinely non-interactive contexts (CI, a
script with no TTY) still need RAD_PASSPHRASE -- and get a fast, clear
failure rather than hanging, since `inquire` returns `Ok(None)` on
`NotTTY` rather than blocking (verified: piping /dev/null as stdin still
fails immediately, not a timeout).

Verified against a real encrypted keystore through an actual pty (via
`script`, since a plain non-interactive shell doesn't have a TTY to
prompt on): the masked "Passphrase:" prompt appears, accepts the typed
passphrase, and produces a correctly encrypted note -- with
RAD_PASSPHRASE unset the entire time.
2026-07-14 17:59:04 +02:00
Maciek "mab122" Bator 1c65ee6230 Fix: git fetch rad can never fetch refs/notes/rad-lfs
Found in a real deployment (mbator.pl blog pipeline): rad lfs init
configures a client-side fetch refspec for refs/notes/rad-lfs, but
git-remote-rad's for_fetch() never advertised any such ref -- notes only
exist per-peer, under refs/namespaces/<peer>/refs/notes/rad-lfs, with no
canonicalization step the way refs/heads/refs/tags get one. Requesting an
unadvertised refspec fails the *entire* git fetch, not just that one ref,
so any repo with `rad lfs init` run could no longer git fetch/pull at
all, LFS or not.

Fix: expose each peer's refs/notes/rad-lfs individually, as
refs/notes/rad-lfs/<peer-id> (list.rs's for_fetch), matching how patch
refs already work rather than trying to canonicalize -- notes are
per-peer contributions (any peer can commit an LFS-tracked file, not
just delegates), so unlike heads there's no single "correct" value to
resolve to; canonicalizing would silently drop non-delegate contributors'
objects. rad lfs init's fetch refspec becomes a wildcard
(+refs/notes/rad-lfs/*:refs/notes/rad-lfs/*); push is unchanged (already
worked -- for_push() turns out to be purely informational for git's
list-for-push handshake, not an enforcement gate, so the actual send-pack
call was never restricted to heads/tags despite what for_push() lists).

rad lfs store/fetch/rekey and seed/unseed's pinning (ipfs.rs::lfs_cids)
now read across every notes ref instead of assuming one canonical ref,
merging recipient lists for notes that share a CID (e.g. after a rekey
performed by a different peer than the original committer) while keeping
genuinely different ciphertexts (from two peers independently encrypting
byte-identical content) as separate candidates to try in turn.

Also added a migration step so re-running `rad lfs init` on an
already-configured repo actually fixes it, by removing the old
non-wildcard fetch refspec rather than just adding the wildcard
alongside a still-broken entry.

Verified through the real git-remote-rad transport (not the storage-copy
shortcut used for earlier testing, since this bug lives specifically in
that transport): fresh identities, a real rad:// remote matching exactly
what `rad init` configures, a genuine `git fetch rad` that used to fail
with "fatal: couldn't find remote ref refs/notes/rad-lfs" now succeeds
and pulls the notes ref, and `git lfs pull` afterward correctly decrypts
private content end-to-end (SHA-256 verified). Re-verified the rekey
flow through the same real path too.
2026-07-14 17:47:15 +02:00
Maciek "mab122" Bator 33ffa12286 Exclude radicle-lfs-transfer submodule from the outer cargo workspace
Cargo's workspace auto-detection was pulling the submodule into the
parent workspace since it's nested under the repo root, breaking
`cargo install --path radicle-lfs-transfer` (the exact command in the
README/LFS-IPFS.md setup instructions) with "current package believes
it's in a workspace when it's not". Discovered while installing the
built binaries onto a real machine.
2026-07-14 16:18:55 +02:00
Maciek "mab122" Bator e060442017 Add client-side encryption for private-repo LFS objects
Radicle's Visibility::Private is enforced entirely by access control at
the replication layer -- git objects for a private repo are plain,
unencrypted objects, protected only by "unauthorized peers are never
given them" (identity/doc.rs). That protection has no jurisdiction over
IPFS's own block-exchange layer: once a legitimate collaborator's IPFS
daemon pins content and joins the network, anyone who obtains the CID by
any means can fetch the plaintext directly.

Adds envelope encryption for private repos: a random per-object content
key encrypts the file (XChaCha20-Poly1305), wrapped once per authorized
recipient (delegates + private allow-list) via ECDH between Radicle
identity keys (already Ed25519, no separate keypair needed) plus HKDF.
The wrapped keys travel in the same refs/notes/rad-lfs note as the CID.

New:
- crates/radicle-cli/src/lfs_crypto.rs -- encryption core (envelope
  schema, encrypt/decrypt, key wrapping, recipient resolution, signer
  loading).
- `rad lfs store`/`rad lfs fetch` -- plumbing commands that now own the
  entire IPFS add/cat + git-notes read/write step (encrypting/decrypting
  as needed), replacing what the pre-commit hook and rad-lfs-transfer
  used to do directly. This collapses what would've become a third copy
  of the note-format logic down to one canonical implementation.
- `rad lfs rekey` -- since Radicle's access control is retroactive (a
  newly-authorized DID can replicate full history) but encrypted objects
  aren't automatically re-wrapped for new recipients, this command lets
  an already-authorized collaborator grant a newly-authorized one access
  to previously-committed objects, without re-encrypting or re-uploading
  content. Revocation remains unsolved, matching Radicle's own model
  (already-replicated/decrypted content can't be un-known).

Operational requirement: private-repo LFS operations need a signer
capable of ECDH, which only an unencrypted keystore or RAD_PASSPHRASE
provides (an ssh-agent-only signer can sign but not do key agreement).
Fails fast with an actionable error rather than hanging on a passphrase
prompt inside a non-interactive git hook.

Breaking change: the refs/notes/rad-lfs note format moves from bare
`cid=<cid>` text to JSON (`{"v":1,"cid":"...","enc":...}`). No migration
path -- prototype, no external users yet.

Verified end-to-end with real Radicle profiles/identities (not just the
transfer-agent protocol): a private repo's IPFS-stored content is
confirmed ciphertext (byte-different from plaintext, +16 bytes AEAD tag);
an authorized peer decrypts correctly; an unauthorized peer is denied
with a clear error and no leaked content; RAD_PASSPHRASE is required and
enforced with a fast, clear failure; and rekey correctly grants a
newly-authorized peer access to a pre-existing object while leaving other
recipients' entries untouched.
2026-07-14 16:01:49 +02:00
Maciek "mab122" Bator 23264b9b52 docs: drop reference to the rejected seed-hosted-server design attempt 2026-07-14 12:53:20 +02:00
Maciek "mab122" Bator 5d53fab673 docs: zero-to-install quick start, Arch dependency lists, fix cross-repo links
Cross-repo markdown links (radicle-lfs-transfer) now use full https URLs
instead of relative paths, since Forgejo can't resolve ../other-repo across
repositories the way a plain filesystem or the git submodule mechanism can.
2026-07-14 12:51:30 +02:00
Maciek "mab122" Bator 005d708059 readme: note this is a fork adding LFS-over-IPFS support 2026-07-14 12:34:49 +02:00
Maciek "mab122" Bator bb8608e984 Add rad lfs init and IPFS-backed pinning for seed/unseed
Adds Git LFS support for repositories, with large file content stored on
each contributor's own local IPFS node instead of a seed-hosted server:

- `rad lfs init`: one-time setup (transfer-agent config, pre-commit hook,
  notes-ref push/fetch refspecs, IPFS daemon reachability check).
- `refs/notes/rad-lfs` carries the oid -> CID mapping, replicating with
  the repository via Radicle's existing ref sync (verified against
  `references_of` in crates/radicle/src/storage/git.rs: every ref is
  replicated except refs/tmp/heads/*, so no protocol changes were needed).
- `rad seed`/`rad unseed` now pin/unpin a repo's known LFS objects in IPFS,
  mirroring Radicle's existing "seeding = keep a full copy" model, and
  warning (not failing) if no local IPFS daemon is reachable.

The actual byte transfer is handled by a separate binary, rad-lfs-transfer
(added here as a submodule), which implements the Git LFS custom-transfer-
agent protocol against a local IPFS daemon. See LFS-IPFS.md for the full
design and setup instructions.

Building and using `rad` for anything other than `rad lfs` requires no
IPFS dependency at all; `rad lfs init` checks for a local daemon upfront
and fails with an actionable message rather than proceeding silently.
2026-07-14 12:32:12 +02:00
Lorenz Leutgeb b5e6d82769
radicle/identity: Tidy up
Small improvements:
 - Remove confusing comments in test
 - Rename `has_active_sibling_accept`
     to   `has_accepted_active_sibling`
 - Small refactoring to avoid `… } else { None }`
2026-07-09 16:26:25 +02:00
Rūdolfs Ošiņš cd57affc36 cargo: Bump zlib-rs to 0.6.5
Fetching a large repository looped with

    Failed to consume the pack sent by the remote

even though the pack appears valid (Git indexes it in full).

The cause appears to be a bug in zlib-rs 0.6.3 according to
<https://github.com/trifectatechfoundation/zlib-rs/pull/504>.
2026-07-09 16:19:33 +02:00
Fintan Halpenny fb80d58af7 CHANGELOG: Describe the changes to repository identity evaluation 2026-07-09 11:35:07 +02:00
Fintan Halpenny 2f1aaa45b2 radicle/cob/identity: Add sibling-accept invariant to property tests 2026-07-08 15:31:45 +01:00
Fintan Halpenny a63f91885f radicle/cob/identity: Guard `IdentityMut::update` against sibling proposals
Ensure that the `IdentityMut::update` API returns an error when it encounters an
active, sibling revision by the same delegate.
2026-07-08 15:31:45 +01:00
Fintan Halpenny b6f07074d9 radicle/cob/identity: Guard `IdentityMut::accept` against sibling accepts
Ensure that the `IdentityMut::accept` API returns an error when it encounters an
active, sibling revision by the same delegate.
2026-07-08 15:31:45 +01:00
Fintan Halpenny 7daeb3135b radicle/cob/identity: Add SiblingAccepted error variant
Add an `ApplyError` for variant, `SiblingAccepted`, to use to enforce the new
invariant: delegates must not accept active, sibling revisions.
2026-07-08 15:31:45 +01:00
Fintan Halpenny 654218e6a5 radicle/cob/identity: Strip implicit author accept for sibling revisions in `action`
When a delegate creates a new `Revision`, their signature is implicitly added to the revision.
Prevent the delegate from an accept verdict on two active, sibling revisions by:
1. Checking that the `state` is `Active`, and then checking `has_active_sibling_accept`
2. If there is an existing verdict, then it the verdict from the new revision is stripped.

The creation of the `Revision` must still happen, as opposed to throwing an
error, in case of existing histories.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 82dc3fe20c radicle/cob/identity: Enforce sibling-accept invariant for `RevisionAccept` in `action`
On `Action::RevisionAccept` use `has_active_sibling_accept` to check for a sibling accept.
If one is found, the `RevisionAccept` is logged and skipped.

This preserves the invariant of a single accept per delegate on active, sibling
revisions without throwing an error on existing histories.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 8e677a9d90 radicle/cob/identity: Add has_active_sibling_accept helper
Add a helper method to check if a delegate has already voted on a sibling
revision.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 62f64744ad radicle/cob/identity: Test accept-before-redact convergence
Companion to the redact-vs-accept test, with reversed timestamp ordering so
accept wins.
2026-07-03 15:22:13 +01:00
Fintan Halpenny f720526fc9 radicle/cob/identity: Test concurrent redact-vs-accept convergence
Verifies that when Alice redacts a revision and Bob concurrently
accepts it, both nodes converge after sync. The CRDT evaluation
order (timestamp-based) determines which operation takes effect.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 21700e4962 radicle/cob/identity: Fix cascading_rejections test to test actual cascading
The test previously had B2 as a sibling of B1 (both children of A1),
because update() always uses self.current as parent. This meant
the test was testing multi-sibling rejection, not cascading.

Now B2 is a child of B1 via a manual transaction, so when B1 is
rejected (sibling E1 accepted), B2 is rejected via RejectedBy::Parent
(cascade), which is what the test name promises.
2026-07-03 15:22:13 +01:00
Lorenz Leutgeb ebf0f417f7 radicle/cob/identity: Deprecate `Identity::id`
In the heartwood workspace, there is just a single use of this function.
Also, the repository ID can be computed on demand, saving memory.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 97f279fe41 radicle/cob/identity: Test redacting accepted revisions
Add a test to ensure that a previously accepted revision cannot be redacted.
2026-07-03 15:22:13 +01:00
Fintan Halpenny e0e231813f radicle/cob/identity: Test `authorization_based_on_parent_not_current`
To reflect that autorization is governed by the causal parent of a
revision, not the currently accepted revision.
2026-07-03 15:22:13 +01:00
Fintan Halpenny 108686e299 radicle/cob/identity: Test queued children with delegate change
Ensure that adopting queued children takes into account that the delegate set
can change.
2026-07-03 15:22:13 +01:00
Adrian Duke da2c523d41 radicle/cob/identity: Add Property Tests
Introduce a property testing harness for asserting a set of properties expected
of the repository identity.

The harness uses the `Network` fixture for providing four nodes that can
interact with the repository identity. The state of the harness is advanced by
providing an actor and operation on the identity document. Invariants are then
asserted about the identity.

The current invariants are:
- The `current` revision is the one and only `Accepted` revision.
- The chain of revisions is valid. That is, `Active` revisions only have a
  parent that is `Accepted`, and not `Rejected` or `Redacted`.
- `Active` revisions do not contain a majority approval.
- `Accepted` revisions contain a majority approval.
- `Rejected` revisions do not contain a majority approval.
- For each revision, at most one child is `Accepted`.
- For each revision, if any of its children is `Accepted`, all other
  children are `Rejected`.
- A revision which is `Rejected(RejectedBy::Parent)` has a parent that
  is `Rejected`.
- A revision which is `Redacted(RedactedBy::Parent)` has a parent that
  is `Redacted`.
- For each revision that is `Rejected` or `Redacted`, none of its
  children is `Active`.
- A sibling or ancestor revision's rejected state applies to its sibling or
  descendant.
- The repository identity documents converge when all nodes have applied all
  operations.
2026-07-03 15:22:13 +01:00
Fintan Halpenny aae8938c01 radicle/test/arbitrary: Make BoundedVec shared
A `BoundedVec` that implements `Arbitrary` is useful for all property testing.
Move it from the `sigrefs` module so that it can be reused by other components.
2026-07-03 15:22:13 +01:00
Adrian Duke 94a24a7283 radicle/cob/identity: Fix comments in test `reject_concurrent`
This incorrectly listed the number of votes and delegates.
2026-07-03 15:22:13 +01:00
Adrian Duke 2768fe79d2 radicle/cob/identity: Test evaluating children of accepted revision
Add a test case to show that when a revision becomes accepted that its children,
which reach a majority vote, are also marked as accepted.
2026-07-03 15:22:13 +01:00